@morojs/moro 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -2
- package/dist/core/config/file-loader.js +31 -25
- package/dist/core/config/file-loader.js.map +1 -1
- package/dist/core/config/schema.d.ts +2 -2
- package/dist/core/config/schema.js +1 -1
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/events/event-bus.js +4 -0
- package/dist/core/events/event-bus.js.map +1 -1
- package/dist/core/http/http-server.d.ts +33 -0
- package/dist/core/http/http-server.js +329 -28
- package/dist/core/http/http-server.js.map +1 -1
- package/dist/core/logger/logger.d.ts +1 -0
- package/dist/core/logger/logger.js +5 -2
- package/dist/core/logger/logger.js.map +1 -1
- package/dist/core/utilities/container.d.ts +1 -0
- package/dist/core/utilities/container.js +11 -1
- package/dist/core/utilities/container.js.map +1 -1
- package/dist/moro.d.ts +8 -0
- package/dist/moro.js +335 -12
- package/dist/moro.js.map +1 -1
- package/dist/types/core.d.ts +17 -0
- package/package.json +14 -10
- package/src/core/config/file-loader.ts +34 -25
- package/src/core/config/schema.ts +1 -1
- package/src/core/events/event-bus.ts +5 -0
- package/src/core/http/http-server.ts +377 -28
- package/src/core/logger/logger.ts +8 -2
- package/src/core/utilities/container.ts +14 -1
- package/src/moro.ts +399 -13
- package/src/types/core.ts +18 -0
|
@@ -23,11 +23,35 @@ export function loadConfigFileSync(cwd: string = process.cwd()): Partial<AppConf
|
|
|
23
23
|
logger.debug('No configuration file found');
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
|
+
// Handle TypeScript files by trying dynamic import (works with tsx/ts-node runtimes)
|
|
27
|
+
if (configFile.endsWith('.ts')) {
|
|
28
|
+
logger.debug('Found TypeScript config file, attempting to load with dynamic import');
|
|
29
|
+
try {
|
|
30
|
+
// When running under tsx/ts-node, dynamic imports work synchronously for TypeScript
|
|
31
|
+
// We can use require() with the current environment that already has TypeScript support
|
|
32
|
+
const config = require(configFile);
|
|
33
|
+
const configData = config.default || config;
|
|
34
|
+
|
|
35
|
+
if (!configData || typeof configData !== 'object') {
|
|
36
|
+
logger.warn(`Configuration file ${configFile} did not export a valid configuration object`);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
logger.info(`TypeScript configuration loaded from: ${configFile}`);
|
|
41
|
+
return configData;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
logger.debug(
|
|
44
|
+
'TypeScript config loading failed in sync mode, this is expected if not running with tsx/ts-node'
|
|
45
|
+
);
|
|
46
|
+
logger.debug('Error details:', String(error));
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
26
50
|
|
|
27
|
-
// Only
|
|
51
|
+
// Only .js files use the standard synchronous loading
|
|
28
52
|
if (!configFile.endsWith('.js')) {
|
|
29
53
|
logger.debug(
|
|
30
|
-
'Found config file
|
|
54
|
+
'Found config file with unsupported extension. Only .js and .ts files are supported.'
|
|
31
55
|
);
|
|
32
56
|
return null;
|
|
33
57
|
}
|
|
@@ -129,7 +153,7 @@ async function importConfigFile(filePath: string): Promise<Partial<AppConfig> |
|
|
|
129
153
|
error.message.includes('Unknown file extension')
|
|
130
154
|
) {
|
|
131
155
|
throw new Error(
|
|
132
|
-
`Failed to load TypeScript config file.
|
|
156
|
+
`Failed to load TypeScript config file. Run your application with tsx or ts-node: "tsx your-app.ts" or "ts-node your-app.ts"`
|
|
133
157
|
);
|
|
134
158
|
}
|
|
135
159
|
throw error;
|
|
@@ -138,30 +162,15 @@ async function importConfigFile(filePath: string): Promise<Partial<AppConfig> |
|
|
|
138
162
|
|
|
139
163
|
/**
|
|
140
164
|
* Setup TypeScript loader for .ts config files
|
|
165
|
+
* Note: This function is intentionally minimal because TypeScript config files
|
|
166
|
+
* should be handled by the runtime environment (tsx, ts-node, etc.) when the
|
|
167
|
+
* user runs their application, not by the framework itself.
|
|
141
168
|
*/
|
|
142
169
|
async function setupTypeScriptLoader(): Promise<void> {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// ts-node might already be registered
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
tsNode.register({
|
|
152
|
-
transpileOnly: true,
|
|
153
|
-
compilerOptions: {
|
|
154
|
-
module: 'commonjs',
|
|
155
|
-
target: 'es2020',
|
|
156
|
-
moduleResolution: 'node',
|
|
157
|
-
allowSyntheticDefaultImports: true,
|
|
158
|
-
esModuleInterop: true,
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
} catch (error) {
|
|
162
|
-
// ts-node not available, try other methods or fail gracefully
|
|
163
|
-
logger.debug('ts-node not available for TypeScript config loading');
|
|
164
|
-
}
|
|
170
|
+
// No-op: TypeScript loading is handled by the runtime environment
|
|
171
|
+
// When users run `tsx moro.config.ts` or `ts-node moro.config.ts`,
|
|
172
|
+
// the TypeScript transpilation is already handled by those tools.
|
|
173
|
+
logger.debug('TypeScript config loading delegated to runtime environment');
|
|
165
174
|
}
|
|
166
175
|
|
|
167
176
|
/**
|
|
@@ -243,7 +243,7 @@ const PerformanceConfigSchema = z.object({
|
|
|
243
243
|
|
|
244
244
|
clustering: z.object({
|
|
245
245
|
enabled: z.coerce.boolean().default(false),
|
|
246
|
-
workers: z.coerce.number().min(1).default(1),
|
|
246
|
+
workers: z.union([z.coerce.number().min(1), z.literal('auto')]).default(1),
|
|
247
247
|
}),
|
|
248
248
|
});
|
|
249
249
|
|
|
@@ -33,6 +33,11 @@ export class MoroEventBus implements GlobalEventBus {
|
|
|
33
33
|
|
|
34
34
|
// Global event emission with full context and metrics
|
|
35
35
|
async emit<T = any>(event: string, data: T, context?: Partial<EventContext>): Promise<boolean> {
|
|
36
|
+
// Fast path: skip processing if no listeners
|
|
37
|
+
if (this.emitter.listenerCount(event) === 0) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
36
41
|
const startTime = Date.now();
|
|
37
42
|
|
|
38
43
|
const fullContext: EventContext = {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// src/core/http-server.ts
|
|
2
2
|
import { IncomingMessage, ServerResponse, createServer, Server } from 'http';
|
|
3
|
-
import { URL } from 'url';
|
|
4
3
|
import * as zlib from 'zlib';
|
|
5
4
|
import { promisify } from 'util';
|
|
6
5
|
import { createFrameworkLogger } from '../logger';
|
|
@@ -17,9 +16,114 @@ export class MoroHttpServer {
|
|
|
17
16
|
private compressionThreshold = 1024;
|
|
18
17
|
private logger = createFrameworkLogger('HttpServer');
|
|
19
18
|
private hookManager: any;
|
|
19
|
+
private requestCounter = 0;
|
|
20
|
+
|
|
21
|
+
// Minimal object pooling for frequently created objects
|
|
22
|
+
private paramObjectPool: Record<string, string>[] = [];
|
|
23
|
+
private bufferPool: Buffer[] = [];
|
|
24
|
+
private readonly maxPoolSize = 50;
|
|
25
|
+
|
|
26
|
+
// Request handler pooling to avoid function creation overhead
|
|
27
|
+
private middlewareExecutionCache = new Map<string, Function>();
|
|
28
|
+
|
|
29
|
+
// String interning for common values (massive memory savings)
|
|
30
|
+
private static readonly INTERNED_METHODS = new Map([
|
|
31
|
+
['GET', 'GET'],
|
|
32
|
+
['POST', 'POST'],
|
|
33
|
+
['PUT', 'PUT'],
|
|
34
|
+
['DELETE', 'DELETE'],
|
|
35
|
+
['PATCH', 'PATCH'],
|
|
36
|
+
['HEAD', 'HEAD'],
|
|
37
|
+
['OPTIONS', 'OPTIONS'],
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
private static readonly INTERNED_HEADERS = new Map([
|
|
41
|
+
['content-type', 'content-type'],
|
|
42
|
+
['content-length', 'content-length'],
|
|
43
|
+
['authorization', 'authorization'],
|
|
44
|
+
['accept', 'accept'],
|
|
45
|
+
['user-agent', 'user-agent'],
|
|
46
|
+
['host', 'host'],
|
|
47
|
+
['connection', 'connection'],
|
|
48
|
+
['cache-control', 'cache-control'],
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
// Pre-compiled response templates for ultra-common responses
|
|
52
|
+
private static readonly RESPONSE_TEMPLATES = {
|
|
53
|
+
notFound: Buffer.from('{"success":false,"error":"Not found"}'),
|
|
54
|
+
unauthorized: Buffer.from('{"success":false,"error":"Unauthorized"}'),
|
|
55
|
+
forbidden: Buffer.from('{"success":false,"error":"Forbidden"}'),
|
|
56
|
+
internalError: Buffer.from('{"success":false,"error":"Internal server error"}'),
|
|
57
|
+
methodNotAllowed: Buffer.from('{"success":false,"error":"Method not allowed"}'),
|
|
58
|
+
rateLimited: Buffer.from('{"success":false,"error":"Rate limit exceeded"}'),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Ultra-fast buffer pool for zero-copy operations (Rust-level performance)
|
|
62
|
+
private static readonly BUFFER_SIZES = [64, 256, 1024, 4096, 16384];
|
|
63
|
+
private static readonly BUFFER_POOLS = new Map<number, Buffer[]>();
|
|
64
|
+
|
|
65
|
+
static {
|
|
66
|
+
// Pre-allocate buffer pools for zero-allocation responses
|
|
67
|
+
for (const size of MoroHttpServer.BUFFER_SIZES) {
|
|
68
|
+
MoroHttpServer.BUFFER_POOLS.set(size, []);
|
|
69
|
+
for (let i = 0; i < 50; i++) {
|
|
70
|
+
// 50 buffers per size
|
|
71
|
+
MoroHttpServer.BUFFER_POOLS.get(size)!.push(Buffer.allocUnsafe(size));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private static getOptimalBuffer(size: number): Buffer {
|
|
77
|
+
// Find the smallest buffer that fits
|
|
78
|
+
for (const poolSize of MoroHttpServer.BUFFER_SIZES) {
|
|
79
|
+
if (size <= poolSize) {
|
|
80
|
+
const pool = MoroHttpServer.BUFFER_POOLS.get(poolSize)!;
|
|
81
|
+
return pool.length > 0 ? pool.pop()! : Buffer.allocUnsafe(poolSize);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return Buffer.allocUnsafe(size);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private static returnBuffer(buffer: Buffer): void {
|
|
88
|
+
// Return buffer to appropriate pool
|
|
89
|
+
const size = buffer.length;
|
|
90
|
+
if (MoroHttpServer.BUFFER_POOLS.has(size)) {
|
|
91
|
+
const pool = MoroHttpServer.BUFFER_POOLS.get(size)!;
|
|
92
|
+
if (pool.length < 50) {
|
|
93
|
+
// Don't let pools grow too large
|
|
94
|
+
pool.push(buffer);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
20
98
|
|
|
21
99
|
constructor() {
|
|
22
100
|
this.server = createServer(this.handleRequest.bind(this));
|
|
101
|
+
|
|
102
|
+
// Optimize server for high performance (conservative settings for compatibility)
|
|
103
|
+
this.server.keepAliveTimeout = 5000; // 5 seconds
|
|
104
|
+
this.server.headersTimeout = 6000; // 6 seconds
|
|
105
|
+
this.server.timeout = 30000; // 30 seconds request timeout
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Configure server for maximum performance (can disable all overhead)
|
|
109
|
+
configurePerformance(
|
|
110
|
+
config: {
|
|
111
|
+
compression?: { enabled: boolean; threshold?: number };
|
|
112
|
+
minimal?: boolean;
|
|
113
|
+
} = {}
|
|
114
|
+
) {
|
|
115
|
+
if (config.compression !== undefined) {
|
|
116
|
+
this.compressionEnabled = config.compression.enabled;
|
|
117
|
+
if (config.compression.threshold !== undefined) {
|
|
118
|
+
this.compressionThreshold = config.compression.threshold;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Minimal mode - disable ALL overhead for pure speed
|
|
123
|
+
if (config.minimal) {
|
|
124
|
+
this.compressionEnabled = false;
|
|
125
|
+
this.compressionThreshold = Infinity; // Never compress
|
|
126
|
+
}
|
|
23
127
|
}
|
|
24
128
|
|
|
25
129
|
// Middleware management
|
|
@@ -58,14 +162,34 @@ export class MoroHttpServer {
|
|
|
58
162
|
const handler = handlers.pop() as HttpHandler;
|
|
59
163
|
const middleware = handlers as Middleware[];
|
|
60
164
|
|
|
61
|
-
|
|
165
|
+
const route = {
|
|
62
166
|
method,
|
|
63
167
|
path,
|
|
64
168
|
pattern,
|
|
65
169
|
paramNames,
|
|
66
170
|
handler,
|
|
67
171
|
middleware,
|
|
68
|
-
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
this.routes.push(route);
|
|
175
|
+
|
|
176
|
+
// Organize routes for optimal lookup
|
|
177
|
+
if (paramNames.length === 0) {
|
|
178
|
+
// Static route - O(1) lookup
|
|
179
|
+
const staticKey = `${method}:${path}`;
|
|
180
|
+
this.staticRoutes.set(staticKey, route);
|
|
181
|
+
} else {
|
|
182
|
+
// Dynamic route - organize by segment count for faster matching
|
|
183
|
+
this.dynamicRoutes.push(route);
|
|
184
|
+
|
|
185
|
+
const segments = path.split('/').filter(s => s.length > 0);
|
|
186
|
+
const segmentCount = segments.length;
|
|
187
|
+
|
|
188
|
+
if (!this.routesBySegmentCount.has(segmentCount)) {
|
|
189
|
+
this.routesBySegmentCount.set(segmentCount, []);
|
|
190
|
+
}
|
|
191
|
+
this.routesBySegmentCount.get(segmentCount)!.push(route);
|
|
192
|
+
}
|
|
69
193
|
}
|
|
70
194
|
|
|
71
195
|
private pathToRegex(path: string): { pattern: RegExp; paramNames: string[] } {
|
|
@@ -90,13 +214,23 @@ export class MoroHttpServer {
|
|
|
90
214
|
const httpRes = this.enhanceResponse(res);
|
|
91
215
|
|
|
92
216
|
try {
|
|
93
|
-
//
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
217
|
+
// Optimized URL and query parsing
|
|
218
|
+
const urlString = req.url!;
|
|
219
|
+
const queryIndex = urlString.indexOf('?');
|
|
220
|
+
|
|
221
|
+
if (queryIndex === -1) {
|
|
222
|
+
// No query string - fast path
|
|
223
|
+
httpReq.path = urlString;
|
|
224
|
+
httpReq.query = {};
|
|
225
|
+
} else {
|
|
226
|
+
// Has query string - parse efficiently
|
|
227
|
+
httpReq.path = urlString.substring(0, queryIndex);
|
|
228
|
+
httpReq.query = this.parseQueryString(urlString.substring(queryIndex + 1));
|
|
229
|
+
}
|
|
97
230
|
|
|
98
|
-
//
|
|
99
|
-
|
|
231
|
+
// Ultra-fast method checking - avoid array includes
|
|
232
|
+
const method = req.method!;
|
|
233
|
+
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
|
|
100
234
|
httpReq.body = await this.parseBody(req);
|
|
101
235
|
}
|
|
102
236
|
|
|
@@ -119,14 +253,19 @@ export class MoroHttpServer {
|
|
|
119
253
|
// Find matching route
|
|
120
254
|
const route = this.findRoute(req.method!, httpReq.path);
|
|
121
255
|
if (!route) {
|
|
122
|
-
|
|
256
|
+
// Ultra-fast 404 response with pre-compiled buffer
|
|
257
|
+
httpRes.statusCode = 404;
|
|
258
|
+
httpRes.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
259
|
+
httpRes.setHeader('Content-Length', MoroHttpServer.RESPONSE_TEMPLATES.notFound.length);
|
|
260
|
+
httpRes.end(MoroHttpServer.RESPONSE_TEMPLATES.notFound);
|
|
123
261
|
return;
|
|
124
262
|
}
|
|
125
263
|
|
|
126
|
-
// Extract path parameters
|
|
264
|
+
// Extract path parameters - optimized with object pooling
|
|
127
265
|
const matches = httpReq.path.match(route.pattern);
|
|
128
266
|
if (matches) {
|
|
129
|
-
|
|
267
|
+
// Use pooled object for parameters
|
|
268
|
+
httpReq.params = this.acquireParamObject();
|
|
130
269
|
route.paramNames.forEach((name, index) => {
|
|
131
270
|
httpReq.params[name] = matches[index + 1];
|
|
132
271
|
});
|
|
@@ -191,14 +330,82 @@ export class MoroHttpServer {
|
|
|
191
330
|
}
|
|
192
331
|
}
|
|
193
332
|
|
|
333
|
+
// Object pooling for parameter objects
|
|
334
|
+
private acquireParamObject(): Record<string, string> {
|
|
335
|
+
const obj = this.paramObjectPool.pop();
|
|
336
|
+
if (obj) {
|
|
337
|
+
// Clear existing properties
|
|
338
|
+
Object.keys(obj).forEach(key => delete obj[key]);
|
|
339
|
+
return obj;
|
|
340
|
+
}
|
|
341
|
+
return {};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private releaseParamObject(params: Record<string, string>): void {
|
|
345
|
+
if (this.paramObjectPool.length < this.maxPoolSize) {
|
|
346
|
+
this.paramObjectPool.push(params);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private acquireBuffer(size: number): Buffer {
|
|
351
|
+
const buffer = this.bufferPool.find(b => b.length >= size);
|
|
352
|
+
if (buffer) {
|
|
353
|
+
this.bufferPool.splice(this.bufferPool.indexOf(buffer), 1);
|
|
354
|
+
return buffer.subarray(0, size);
|
|
355
|
+
}
|
|
356
|
+
return Buffer.allocUnsafe(size);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private releaseBuffer(buffer: Buffer): void {
|
|
360
|
+
if (this.bufferPool.length < this.maxPoolSize && buffer.length <= 8192) {
|
|
361
|
+
this.bufferPool.push(buffer);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private streamLargeResponse(res: any, data: any): void {
|
|
366
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
367
|
+
res.setHeader('Transfer-Encoding', 'chunked');
|
|
368
|
+
|
|
369
|
+
// Stream the response in chunks
|
|
370
|
+
const jsonString = JSON.stringify(data);
|
|
371
|
+
const chunkSize = 8192; // 8KB chunks
|
|
372
|
+
|
|
373
|
+
for (let i = 0; i < jsonString.length; i += chunkSize) {
|
|
374
|
+
const chunk = jsonString.substring(i, i + chunkSize);
|
|
375
|
+
res.write(chunk);
|
|
376
|
+
}
|
|
377
|
+
res.end();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private normalizePath(path: string): string {
|
|
381
|
+
// Check cache first
|
|
382
|
+
if (this.pathNormalizationCache.has(path)) {
|
|
383
|
+
return this.pathNormalizationCache.get(path)!;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Fast normalization: remove trailing slash (except root), decode once
|
|
387
|
+
let normalized = path;
|
|
388
|
+
if (normalized.length > 1 && normalized.endsWith('/')) {
|
|
389
|
+
normalized = normalized.slice(0, -1);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Cache result (limit cache size)
|
|
393
|
+
if (this.pathNormalizationCache.size < 200) {
|
|
394
|
+
this.pathNormalizationCache.set(path, normalized);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return normalized;
|
|
398
|
+
}
|
|
399
|
+
|
|
194
400
|
private enhanceRequest(req: IncomingMessage): HttpRequest {
|
|
195
401
|
const httpReq = req as HttpRequest;
|
|
196
|
-
httpReq.params =
|
|
402
|
+
httpReq.params = this.acquireParamObject();
|
|
197
403
|
httpReq.query = {};
|
|
198
404
|
httpReq.body = null;
|
|
199
405
|
httpReq.path = '';
|
|
200
406
|
httpReq.ip = req.socket.remoteAddress || '';
|
|
201
|
-
|
|
407
|
+
// Faster request ID generation
|
|
408
|
+
httpReq.requestId = Date.now().toString(36) + (++this.requestCounter).toString(36);
|
|
202
409
|
httpReq.headers = req.headers as Record<string, string>;
|
|
203
410
|
|
|
204
411
|
// Parse cookies
|
|
@@ -233,32 +440,94 @@ export class MoroHttpServer {
|
|
|
233
440
|
httpRes.json = async (data: any) => {
|
|
234
441
|
if (httpRes.headersSent) return;
|
|
235
442
|
|
|
236
|
-
|
|
237
|
-
|
|
443
|
+
// Ultra-fast JSON serialization with zero-copy buffers
|
|
444
|
+
let jsonString: string;
|
|
445
|
+
|
|
446
|
+
// Enhanced JSON optimization for common API patterns
|
|
447
|
+
if (data && typeof data === 'object' && 'success' in data) {
|
|
448
|
+
if ('data' in data && 'error' in data && !('total' in data)) {
|
|
449
|
+
// {success, data, error} pattern
|
|
450
|
+
jsonString = `{"success":${data.success},"data":${JSON.stringify(data.data)},"error":${JSON.stringify(data.error)}}`;
|
|
451
|
+
} else if ('data' in data && 'total' in data && !('error' in data)) {
|
|
452
|
+
// {success, data, total} pattern
|
|
453
|
+
jsonString = `{"success":${data.success},"data":${JSON.stringify(data.data)},"total":${data.total}}`;
|
|
454
|
+
} else if ('data' in data && !('error' in data) && !('total' in data)) {
|
|
455
|
+
// {success, data} pattern
|
|
456
|
+
jsonString = `{"success":${data.success},"data":${JSON.stringify(data.data)}}`;
|
|
457
|
+
} else if ('error' in data && !('data' in data) && !('total' in data)) {
|
|
458
|
+
// {success, error} pattern
|
|
459
|
+
jsonString = `{"success":${data.success},"error":${JSON.stringify(data.error)}}`;
|
|
460
|
+
} else {
|
|
461
|
+
// Complex object - use standard JSON.stringify
|
|
462
|
+
jsonString = JSON.stringify(data);
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
jsonString = JSON.stringify(data);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Use buffer pool for zero-allocation responses
|
|
469
|
+
const estimatedSize = jsonString.length;
|
|
470
|
+
if (estimatedSize > 32768) {
|
|
471
|
+
// Large response - stream it
|
|
472
|
+
return this.streamLargeResponse(httpRes, data);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const buffer = MoroHttpServer.getOptimalBuffer(estimatedSize);
|
|
476
|
+
const actualLength = buffer.write(jsonString, 0, 'utf8');
|
|
238
477
|
|
|
239
|
-
|
|
478
|
+
// Slice to actual size to avoid sending extra bytes
|
|
479
|
+
const finalBuffer =
|
|
480
|
+
actualLength === buffer.length ? buffer : buffer.subarray(0, actualLength);
|
|
240
481
|
|
|
241
|
-
//
|
|
242
|
-
|
|
482
|
+
// Optimized header setting - set multiple headers at once when possible
|
|
483
|
+
const headers: Record<string, string | number> = {
|
|
484
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// Compression with buffer pool
|
|
488
|
+
if (this.compressionEnabled && finalBuffer.length > this.compressionThreshold) {
|
|
243
489
|
const acceptEncoding = httpRes.req.headers['accept-encoding'] || '';
|
|
244
490
|
|
|
245
491
|
if (acceptEncoding.includes('gzip')) {
|
|
246
|
-
const compressed = await gzip(
|
|
247
|
-
|
|
248
|
-
|
|
492
|
+
const compressed = await gzip(finalBuffer);
|
|
493
|
+
headers['Content-Encoding'] = 'gzip';
|
|
494
|
+
headers['Content-Length'] = compressed.length;
|
|
495
|
+
|
|
496
|
+
// Set all headers at once
|
|
497
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
498
|
+
httpRes.setHeader(key, value);
|
|
499
|
+
});
|
|
500
|
+
|
|
249
501
|
httpRes.end(compressed);
|
|
502
|
+
// Return buffer to pool after response
|
|
503
|
+
process.nextTick(() => MoroHttpServer.returnBuffer(buffer));
|
|
250
504
|
return;
|
|
251
505
|
} else if (acceptEncoding.includes('deflate')) {
|
|
252
|
-
const compressed = await deflate(
|
|
253
|
-
|
|
254
|
-
|
|
506
|
+
const compressed = await deflate(finalBuffer);
|
|
507
|
+
headers['Content-Encoding'] = 'deflate';
|
|
508
|
+
headers['Content-Length'] = compressed.length;
|
|
509
|
+
|
|
510
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
511
|
+
httpRes.setHeader(key, value);
|
|
512
|
+
});
|
|
513
|
+
|
|
255
514
|
httpRes.end(compressed);
|
|
515
|
+
// Return buffer to pool after response
|
|
516
|
+
process.nextTick(() => MoroHttpServer.returnBuffer(buffer));
|
|
256
517
|
return;
|
|
257
518
|
}
|
|
258
519
|
}
|
|
259
520
|
|
|
260
|
-
|
|
261
|
-
|
|
521
|
+
headers['Content-Length'] = finalBuffer.length;
|
|
522
|
+
|
|
523
|
+
// Set all headers at once for better performance
|
|
524
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
525
|
+
httpRes.setHeader(key, value);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
httpRes.end(finalBuffer);
|
|
529
|
+
// Return buffer to pool after response (zero-copy achievement!)
|
|
530
|
+
process.nextTick(() => MoroHttpServer.returnBuffer(buffer));
|
|
262
531
|
};
|
|
263
532
|
|
|
264
533
|
httpRes.send = (data: string | Buffer) => {
|
|
@@ -485,8 +754,85 @@ export class MoroHttpServer {
|
|
|
485
754
|
return result;
|
|
486
755
|
}
|
|
487
756
|
|
|
757
|
+
private parseQueryString(queryString: string): Record<string, string> {
|
|
758
|
+
const result: Record<string, string> = {};
|
|
759
|
+
if (!queryString) return result;
|
|
760
|
+
|
|
761
|
+
const pairs = queryString.split('&');
|
|
762
|
+
for (const pair of pairs) {
|
|
763
|
+
const equalIndex = pair.indexOf('=');
|
|
764
|
+
if (equalIndex === -1) {
|
|
765
|
+
result[decodeURIComponent(pair)] = '';
|
|
766
|
+
} else {
|
|
767
|
+
const key = decodeURIComponent(pair.substring(0, equalIndex));
|
|
768
|
+
const value = decodeURIComponent(pair.substring(equalIndex + 1));
|
|
769
|
+
result[key] = value;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return result;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Advanced route optimization: cache + static routes + segment grouping
|
|
776
|
+
private routeCache = new Map<string, RouteEntry | null>();
|
|
777
|
+
private staticRoutes = new Map<string, RouteEntry>();
|
|
778
|
+
private dynamicRoutes: RouteEntry[] = [];
|
|
779
|
+
private routesBySegmentCount = new Map<number, RouteEntry[]>();
|
|
780
|
+
private pathNormalizationCache = new Map<string, string>();
|
|
781
|
+
|
|
782
|
+
// Ultra-fast CPU cache-friendly optimizations (Rust-level performance)
|
|
783
|
+
private routeHitCount = new Map<string, number>(); // Track route popularity for cache optimization
|
|
784
|
+
private static readonly HOT_ROUTE_THRESHOLD = 100; // Routes accessed 100+ times get hot path treatment
|
|
785
|
+
|
|
488
786
|
private findRoute(method: string, path: string): RouteEntry | null {
|
|
489
|
-
|
|
787
|
+
// Normalize path for consistent matching
|
|
788
|
+
const normalizedPath = this.normalizePath(path);
|
|
789
|
+
const cacheKey = `${method}:${normalizedPath}`;
|
|
790
|
+
|
|
791
|
+
// Track route popularity for hot path optimization
|
|
792
|
+
const hitCount = (this.routeHitCount.get(cacheKey) || 0) + 1;
|
|
793
|
+
this.routeHitCount.set(cacheKey, hitCount);
|
|
794
|
+
|
|
795
|
+
// Check cache first (hot path optimization)
|
|
796
|
+
if (this.routeCache.has(cacheKey)) {
|
|
797
|
+
const cachedRoute = this.routeCache.get(cacheKey)!;
|
|
798
|
+
|
|
799
|
+
// Promote frequently accessed routes to front of cache (LRU-like)
|
|
800
|
+
if (hitCount > MoroHttpServer.HOT_ROUTE_THRESHOLD && this.routeCache.size > 100) {
|
|
801
|
+
this.routeCache.delete(cacheKey);
|
|
802
|
+
this.routeCache.set(cacheKey, cachedRoute); // Move to end (most recent)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return cachedRoute;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Phase 1: O(1) static route lookup
|
|
809
|
+
const staticRoute = this.staticRoutes.get(cacheKey);
|
|
810
|
+
if (staticRoute) {
|
|
811
|
+
this.routeCache.set(cacheKey, staticRoute);
|
|
812
|
+
return staticRoute;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Phase 2: Optimized dynamic route matching by segment count
|
|
816
|
+
let route: RouteEntry | null = null;
|
|
817
|
+
if (this.dynamicRoutes.length > 0) {
|
|
818
|
+
const segments = normalizedPath.split('/').filter(s => s.length > 0);
|
|
819
|
+
const candidateRoutes = this.routesBySegmentCount.get(segments.length) || this.dynamicRoutes;
|
|
820
|
+
|
|
821
|
+
// Only test routes with matching method and segment count
|
|
822
|
+
for (const candidateRoute of candidateRoutes) {
|
|
823
|
+
if (candidateRoute.method === method && candidateRoute.pattern.test(normalizedPath)) {
|
|
824
|
+
route = candidateRoute;
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Cache result (limit cache size to prevent memory leaks)
|
|
831
|
+
if (this.routeCache.size < 500) {
|
|
832
|
+
this.routeCache.set(cacheKey, route);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return route;
|
|
490
836
|
}
|
|
491
837
|
|
|
492
838
|
private async executeMiddleware(
|
|
@@ -495,6 +841,9 @@ export class MoroHttpServer {
|
|
|
495
841
|
res: HttpResponse
|
|
496
842
|
): Promise<void> {
|
|
497
843
|
for (const mw of middleware) {
|
|
844
|
+
// Short-circuit if response already sent
|
|
845
|
+
if (res.headersSent) return;
|
|
846
|
+
|
|
498
847
|
await new Promise<void>((resolve, reject) => {
|
|
499
848
|
let nextCalled = false;
|
|
500
849
|
|
|
@@ -29,6 +29,7 @@ export class MoroLogger implements Logger {
|
|
|
29
29
|
private startTime = Date.now();
|
|
30
30
|
private contextPrefix?: string;
|
|
31
31
|
private contextMetadata?: Record<string, any>;
|
|
32
|
+
private parent?: MoroLogger; // Reference to parent logger for level inheritance
|
|
32
33
|
|
|
33
34
|
private static readonly LEVELS: Record<LogLevel, number> = {
|
|
34
35
|
debug: 0,
|
|
@@ -129,6 +130,10 @@ export class MoroLogger implements Logger {
|
|
|
129
130
|
childLogger.contextMetadata = { ...this.contextMetadata, ...metadata };
|
|
130
131
|
childLogger.outputs = this.outputs;
|
|
131
132
|
childLogger.filters = this.filters;
|
|
133
|
+
|
|
134
|
+
// Keep reference to parent for level inheritance
|
|
135
|
+
(childLogger as any).parent = this;
|
|
136
|
+
|
|
132
137
|
return childLogger;
|
|
133
138
|
}
|
|
134
139
|
|
|
@@ -190,8 +195,9 @@ export class MoroLogger implements Logger {
|
|
|
190
195
|
context?: string,
|
|
191
196
|
metadata?: Record<string, any>
|
|
192
197
|
): void {
|
|
193
|
-
// Check level threshold
|
|
194
|
-
|
|
198
|
+
// Check level threshold - use parent level if available (for child loggers)
|
|
199
|
+
const effectiveLevel = this.parent ? this.parent.level : this.level;
|
|
200
|
+
if (MoroLogger.LEVELS[level] < MoroLogger.LEVELS[effectiveLevel as LogLevel]) {
|
|
195
201
|
return;
|
|
196
202
|
}
|
|
197
203
|
|
|
@@ -699,6 +699,7 @@ export class ServiceRegistrationBuilder<T> {
|
|
|
699
699
|
// Standard Container class
|
|
700
700
|
export class Container {
|
|
701
701
|
private functionalContainer = new FunctionalContainer();
|
|
702
|
+
private resolutionCache = new Map<string, any>();
|
|
702
703
|
|
|
703
704
|
register<T>(name: string, factory: () => T, singleton = false): void {
|
|
704
705
|
this.functionalContainer
|
|
@@ -709,7 +710,19 @@ export class Container {
|
|
|
709
710
|
}
|
|
710
711
|
|
|
711
712
|
resolve<T>(name: string): T {
|
|
712
|
-
|
|
713
|
+
// Fast path for cached resolutions
|
|
714
|
+
if (this.resolutionCache.has(name)) {
|
|
715
|
+
return this.resolutionCache.get(name);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const resolved = this.functionalContainer.resolveSync<T>(name);
|
|
719
|
+
|
|
720
|
+
// Cache result (limit cache size)
|
|
721
|
+
if (this.resolutionCache.size < 50) {
|
|
722
|
+
this.resolutionCache.set(name, resolved);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return resolved;
|
|
713
726
|
}
|
|
714
727
|
|
|
715
728
|
has(name: string): boolean {
|