thyscript 1.0.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.
Files changed (49) hide show
  1. package/bin/thy-node.js +332 -0
  2. package/bin/thy.js +828 -0
  3. package/bin/thys.js +25 -0
  4. package/examples/calculator.thy +274 -0
  5. package/examples/calculator.thy.js +628 -0
  6. package/examples/hello.thy +34 -0
  7. package/examples/login.thy +382 -0
  8. package/examples/login.thy.js +710 -0
  9. package/package.json +54 -0
  10. package/scripts/_class.js +18 -0
  11. package/scripts/_class2.js +11 -0
  12. package/scripts/_class3.js +21 -0
  13. package/scripts/_dstruct.js +20 -0
  14. package/scripts/_dstruct2.js +24 -0
  15. package/scripts/_scaffold.js +16 -0
  16. package/scripts/_scaffold2.js +16 -0
  17. package/scripts/_scaffold3.js +26 -0
  18. package/scripts/_tpl_compile.js +13 -0
  19. package/scripts/_tpl_test.js +9 -0
  20. package/scripts/_typeof.js +26 -0
  21. package/scripts/_typeof2.js +25 -0
  22. package/scripts/patch_genmethod.js +44 -0
  23. package/scripts/smoke_test.js +167 -0
  24. package/scripts/smoke_test2.js +151 -0
  25. package/scripts/test_control_panel.js +58 -0
  26. package/src/compiler/CodeGenerator.js +1449 -0
  27. package/src/compiler/JITCompiler.js +325 -0
  28. package/src/database/DatabaseEngine.js +931 -0
  29. package/src/errors/ErrorDetector.js +305 -0
  30. package/src/errors/ErrorMessages.js +497 -0
  31. package/src/index.js +169 -0
  32. package/src/lexer/Lexer.js +342 -0
  33. package/src/lexer/TokenTypes.js +1234 -0
  34. package/src/memory/MemoryManager.js +283 -0
  35. package/src/parser/AST.js +606 -0
  36. package/src/parser/Parser.js +1942 -0
  37. package/src/runtime/ThyRuntime.js +265 -0
  38. package/src/runtime/stdlib/ArrayLib.js +207 -0
  39. package/src/runtime/stdlib/FileLib.js +168 -0
  40. package/src/runtime/stdlib/MathLib.js +411 -0
  41. package/src/runtime/stdlib/StringLib.js +387 -0
  42. package/src/server/ControlPanel.js +1188 -0
  43. package/src/server/HTMLCSSEngine.js +866 -0
  44. package/src/server/SecurityEngine.js +714 -0
  45. package/src/server/ServerEngine.js +738 -0
  46. package/tests/test1_math.thy +82 -0
  47. package/tests/test2_oop.thy +133 -0
  48. package/tests/test3_server.thy +143 -0
  49. package/tests/test4_functions.thy +159 -0
@@ -0,0 +1,738 @@
1
+ 'use strict';
2
+
3
+ const http = require('http');
4
+ const https = require('https');
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+ const cluster = require('cluster');
8
+ const os = require('os');
9
+
10
+ // Express with built-in high-speed native HTTP fallback
11
+ let express;
12
+ try {
13
+ express = require('express');
14
+ } catch (e) {
15
+ express = () => {
16
+ const routes = [];
17
+ const middlewares = [];
18
+ const app = {
19
+ use(fn) { middlewares.push(fn); return app; },
20
+ get(route, fn) { routes.push({ method: 'GET', route, fn }); return app; },
21
+ post(route, fn) { routes.push({ method: 'POST', route, fn }); return app; },
22
+ put(route, fn) { routes.push({ method: 'PUT', route, fn }); return app; },
23
+ delete(route, fn) { routes.push({ method: 'DELETE', route, fn }); return app; },
24
+ set() { return app; },
25
+ listen(port, cb) {
26
+ const server = http.createServer((req, res) => {
27
+ res.json = (data) => {
28
+ res.setHeader('Content-Type', 'application/json');
29
+ res.end(JSON.stringify(data));
30
+ };
31
+ res.send = (body) => {
32
+ if (typeof body === 'object') return res.json(body);
33
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
34
+ res.end(String(body));
35
+ };
36
+ res.status = (code) => { res.statusCode = code; return res; };
37
+ const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
38
+ req.path = parsedUrl.pathname;
39
+ req.query = Object.fromEntries(parsedUrl.searchParams);
40
+ let bodyData = '';
41
+ req.on('data', chunk => { bodyData += chunk; });
42
+ req.on('end', () => {
43
+ try { req.body = JSON.parse(bodyData); } catch (_) { req.body = bodyData; }
44
+ let i = 0;
45
+ const next = () => {
46
+ if (i < middlewares.length) {
47
+ middlewares[i++](req, res, next);
48
+ } else {
49
+ const match = routes.find(r => (r.method === req.method || r.method === 'ALL') && (r.route === req.path || r.route === '*'));
50
+ if (match) {
51
+ match.fn(req, res);
52
+ } else {
53
+ res.statusCode = 404;
54
+ res.end('Not Found');
55
+ }
56
+ }
57
+ };
58
+ next();
59
+ });
60
+ });
61
+ server.listen(port, cb);
62
+ return server;
63
+ }
64
+ };
65
+ return app;
66
+ };
67
+ express.json = () => (req, res, next) => next();
68
+ express.urlencoded = () => (req, res, next) => next();
69
+ express.static = (dir) => (req, res, next) => next();
70
+ }
71
+
72
+ // Optional dependencies with graceful fallback
73
+ let socketIo, compression, cors;
74
+ try { socketIo = require('socket.io'); } catch (e) { socketIo = null; }
75
+ try { compression = require('compression'); } catch (e) { compression = (req, res, next) => next(); }
76
+ try { cors = require('cors'); } catch (e) { cors = (opts) => (req, res, next) => next(); }
77
+
78
+ /**
79
+ * ThyScript ServerEngine
80
+ * High-performance HTTP/HTTPS/WebSocket server built on Express.
81
+ */
82
+ class ServerEngine {
83
+ constructor() {
84
+ this.app = null;
85
+ this.server = null;
86
+ this.io = null;
87
+ this.isRunning = false;
88
+ this.startTime = null;
89
+
90
+ // Metrics
91
+ this.metrics = {
92
+ totalRequests: 0,
93
+ activeRequests: 0,
94
+ totalErrors: 0,
95
+ totalLatencyMs: 0,
96
+ requestsPerSecond: 0,
97
+ statusCodes: {},
98
+ routes: {},
99
+ };
100
+
101
+ // Rate limiting store
102
+ this._rateLimitStore = new Map(); // key -> { count, resetAt }
103
+
104
+ // XSS patterns
105
+ this._xssPatterns = [
106
+ /<script[\s\S]*?>[\s\S]*?<\/script>/gi,
107
+ /javascript\s*:/gi,
108
+ /on\w+\s*=/gi,
109
+ /<iframe[\s\S]*?>/gi,
110
+ /<object[\s\S]*?>/gi,
111
+ /<embed[\s\S]*?>/gi,
112
+ /eval\s*\(/gi,
113
+ /expression\s*\(/gi,
114
+ ];
115
+
116
+ // SQL injection patterns
117
+ this._sqlPatterns = [
118
+ /(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|UNION|TRUNCATE)\b)/gi,
119
+ /('|")\s*;\s*--/gi,
120
+ /\/\*[\s\S]*?\*\//g,
121
+ /\bOR\b\s+\d+\s*=\s*\d+/gi,
122
+ /\bAND\b\s+\d+\s*=\s*\d+/gi,
123
+ ];
124
+
125
+ // Request queue for high-load scenarios
126
+ this._requestQueue = [];
127
+ this._queueMaxSize = 1000;
128
+ this._queueProcessing = false;
129
+ this._queueConcurrency = 50;
130
+ this._activeQueuedRequests = 0;
131
+
132
+ // SSE clients
133
+ this._sseClients = new Map(); // clientId -> res
134
+
135
+ // WebSocket rooms
136
+ this._rooms = new Map(); // roomName -> Set<socket>
137
+
138
+ // Blocked IPs
139
+ this._blockedIPs = new Set();
140
+
141
+ // Metrics ticker
142
+ this._metricsInterval = null;
143
+ this._lastRequestCount = 0;
144
+ }
145
+
146
+ // ─────────────────────────────────────────────
147
+ // Core Setup
148
+ // ─────────────────────────────────────────────
149
+
150
+ /**
151
+ * Creates the Express application and HTTP server.
152
+ * @param {number} port - Default port.
153
+ * @param {object} options - Options: { maxBodySize, trustProxy, queueMaxSize, queueConcurrency }
154
+ * @returns {ServerEngine}
155
+ */
156
+ create(port = 3000, options = {}) {
157
+ this.defaultPort = port;
158
+ this.options = Object.assign({
159
+ maxBodySize: '10mb',
160
+ trustProxy: false,
161
+ queueMaxSize: 1000,
162
+ queueConcurrency: 50,
163
+ enableRequestQueue: false,
164
+ }, options);
165
+
166
+ this._queueMaxSize = this.options.queueMaxSize;
167
+ this._queueConcurrency = this.options.queueConcurrency;
168
+
169
+ this.app = express();
170
+
171
+ if (this.options.trustProxy) {
172
+ this.app.set('trust proxy', 1);
173
+ }
174
+
175
+ // Body parsers
176
+ this.app.use(express.json({ limit: this.options.maxBodySize }));
177
+ this.app.use(express.urlencoded({ extended: true, limit: this.options.maxBodySize }));
178
+
179
+ // Built-in preprocessing middleware
180
+ this.app.use(this._preprocessRequest.bind(this));
181
+
182
+ // Health check
183
+ this.app.get('/health', (req, res) => {
184
+ res.json({
185
+ status: 'ok',
186
+ uptime: this.startTime ? Math.floor((Date.now() - this.startTime) / 1000) : 0,
187
+ timestamp: new Date().toISOString(),
188
+ pid: process.pid,
189
+ memory: process.memoryUsage(),
190
+ version: process.version,
191
+ });
192
+ });
193
+
194
+ // Metrics endpoint
195
+ this.app.get('/metrics', (req, res) => {
196
+ const uptime = this.startTime ? (Date.now() - this.startTime) : 1;
197
+ res.json({
198
+ ...this.metrics,
199
+ uptime,
200
+ avgLatencyMs: this.metrics.totalRequests > 0
201
+ ? (this.metrics.totalLatencyMs / this.metrics.totalRequests).toFixed(3)
202
+ : 0,
203
+ memoryUsage: process.memoryUsage(),
204
+ cpuUsage: process.cpuUsage(),
205
+ pid: process.pid,
206
+ });
207
+ });
208
+
209
+ // Custom error handler
210
+ this.app.use(this._errorHandler.bind(this));
211
+
212
+ this.server = http.createServer(this.app);
213
+ return this;
214
+ }
215
+
216
+ // ─────────────────────────────────────────────
217
+ // Middleware
218
+ // ─────────────────────────────────────────────
219
+
220
+ /**
221
+ * Adds an Express middleware function.
222
+ * @param {function} fn - Express middleware (req, res, next).
223
+ * @returns {ServerEngine}
224
+ */
225
+ addMiddleware(fn) {
226
+ if (!this.app) throw new Error('Call create() first');
227
+ this.app.use(fn);
228
+ return this;
229
+ }
230
+
231
+ /**
232
+ * Enables CORS support.
233
+ * @param {object} options - cors() options.
234
+ * @returns {ServerEngine}
235
+ */
236
+ enableCORS(options = {}) {
237
+ if (!this.app) throw new Error('Call create() first');
238
+ const defaultOptions = {
239
+ origin: '*',
240
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
241
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
242
+ credentials: true,
243
+ optionsSuccessStatus: 200,
244
+ };
245
+ this.app.use(cors(Object.assign(defaultOptions, options)));
246
+ return this;
247
+ }
248
+
249
+ /**
250
+ * Enables gzip/deflate compression.
251
+ * @returns {ServerEngine}
252
+ */
253
+ enableCompression() {
254
+ if (!this.app) throw new Error('Call create() first');
255
+ this.app.use(compression());
256
+ return this;
257
+ }
258
+
259
+ /**
260
+ * Serves static files from a directory.
261
+ * @param {string} dir - Absolute or relative directory path.
262
+ * @param {string} [mountPath='/'] - URL prefix.
263
+ * @param {object} [options={}] - express.static options.
264
+ * @returns {ServerEngine}
265
+ */
266
+ enableStaticFiles(dir, mountPath = '/', options = {}) {
267
+ if (!this.app) throw new Error('Call create() first');
268
+ const staticOptions = Object.assign({
269
+ maxAge: '1d',
270
+ etag: true,
271
+ lastModified: true,
272
+ }, options);
273
+ this.app.use(mountPath, express.static(path.resolve(dir), staticOptions));
274
+ return this;
275
+ }
276
+
277
+ /**
278
+ * Upgrades the server to HTTPS.
279
+ * @param {string|Buffer} cert - Path to cert file or cert Buffer.
280
+ * @param {string|Buffer} key - Path to key file or key Buffer.
281
+ * @returns {ServerEngine}
282
+ */
283
+ enableSSL(cert, key) {
284
+ if (!this.app) throw new Error('Call create() first');
285
+ const certData = typeof cert === 'string' ? fs.readFileSync(cert) : cert;
286
+ const keyData = typeof key === 'string' ? fs.readFileSync(key) : key;
287
+ this.server = https.createServer({ cert: certData, key: keyData }, this.app);
288
+ return this;
289
+ }
290
+
291
+ // ─────────────────────────────────────────────
292
+ // Routing
293
+ // ─────────────────────────────────────────────
294
+
295
+ /**
296
+ * Adds a route to the Express application.
297
+ * @param {string} method - HTTP method (get, post, put, delete, patch, etc.)
298
+ * @param {string} routePath - Route path.
299
+ * @param {...function} handlers - Route handler(s).
300
+ * @returns {ServerEngine}
301
+ */
302
+ addRoute(method, routePath, ...handlers) {
303
+ if (!this.app) throw new Error('Call create() first');
304
+ const m = method.toLowerCase();
305
+ if (typeof this.app[m] !== 'function') {
306
+ throw new Error(`Invalid HTTP method: ${method}`);
307
+ }
308
+ // Wrap handlers for metrics tracking
309
+ const wrappedHandlers = handlers.map((handler, idx) => {
310
+ if (idx < handlers.length - 1) return handler; // pass-through for middleware chain
311
+ return (req, res, next) => {
312
+ const routeKey = `${method.toUpperCase()} ${routePath}`;
313
+ this.metrics.routes[routeKey] = (this.metrics.routes[routeKey] || 0) + 1;
314
+ return handler(req, res, next);
315
+ };
316
+ });
317
+ this.app[m](routePath, ...wrappedHandlers);
318
+ return this;
319
+ }
320
+
321
+ // ─────────────────────────────────────────────
322
+ // WebSocket
323
+ // ─────────────────────────────────────────────
324
+
325
+ /**
326
+ * Enables Socket.IO WebSocket support.
327
+ * @param {object} options - Socket.IO server options.
328
+ * @returns {ServerEngine}
329
+ */
330
+ enableWebSocket(options = {}) {
331
+ if (!socketIo) throw new Error('socket.io is not installed. Run: npm install socket.io');
332
+ if (!this.server) throw new Error('Call create() first');
333
+
334
+ const ioOptions = Object.assign({
335
+ cors: { origin: '*', methods: ['GET', 'POST'] },
336
+ pingTimeout: 60000,
337
+ pingInterval: 25000,
338
+ }, options);
339
+
340
+ this.io = new socketIo.Server(this.server, ioOptions);
341
+
342
+ this.io.on('connection', (socket) => {
343
+ this.metrics.totalRequests++;
344
+
345
+ socket.on('join-room', (room) => {
346
+ this._joinRoom(socket, room);
347
+ });
348
+
349
+ socket.on('leave-room', (room) => {
350
+ this._leaveRoom(socket, room);
351
+ });
352
+
353
+ socket.on('room-message', ({ room, message }) => {
354
+ this._broadcastToRoom(room, 'message', message, socket.id);
355
+ });
356
+
357
+ socket.on('disconnect', () => {
358
+ this._rooms.forEach((sockets, room) => {
359
+ sockets.delete(socket.id);
360
+ if (sockets.size === 0) this._rooms.delete(room);
361
+ });
362
+ });
363
+ });
364
+
365
+ return this;
366
+ }
367
+
368
+ // ─────────────────────────────────────────────
369
+ // WebSocket Room Management
370
+ // ─────────────────────────────────────────────
371
+
372
+ _joinRoom(socket, room) {
373
+ socket.join(room);
374
+ if (!this._rooms.has(room)) this._rooms.set(room, new Set());
375
+ this._rooms.get(room).add(socket.id);
376
+ socket.emit('joined-room', { room, members: this._rooms.get(room).size });
377
+ socket.to(room).emit('user-joined', { room, socketId: socket.id });
378
+ }
379
+
380
+ _leaveRoom(socket, room) {
381
+ socket.leave(room);
382
+ if (this._rooms.has(room)) {
383
+ this._rooms.get(room).delete(socket.id);
384
+ if (this._rooms.get(room).size === 0) this._rooms.delete(room);
385
+ }
386
+ socket.to(room).emit('user-left', { room, socketId: socket.id });
387
+ }
388
+
389
+ _broadcastToRoom(room, event, data, excludeSocketId = null) {
390
+ if (!this.io) return;
391
+ if (excludeSocketId) {
392
+ this.io.to(room).except(excludeSocketId).emit(event, data);
393
+ } else {
394
+ this.io.to(room).emit(event, data);
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Broadcast to all sockets in a room.
400
+ * @param {string} room
401
+ * @param {string} event
402
+ * @param {*} data
403
+ */
404
+ broadcastToRoom(room, event, data) {
405
+ this._broadcastToRoom(room, event, data);
406
+ return this;
407
+ }
408
+
409
+ /**
410
+ * Returns info about all active WebSocket rooms.
411
+ * @returns {object}
412
+ */
413
+ getRooms() {
414
+ const result = {};
415
+ this._rooms.forEach((sockets, room) => {
416
+ result[room] = { members: sockets.size, socketIds: [...sockets] };
417
+ });
418
+ return result;
419
+ }
420
+
421
+ // ─────────────────────────────────────────────
422
+ // Server-Sent Events (SSE)
423
+ // ─────────────────────────────────────────────
424
+
425
+ /**
426
+ * Sets up an SSE endpoint.
427
+ * Clients connect via GET to `sseRoutePath`.
428
+ * Use `sendSSE(clientId, event, data)` to push events.
429
+ * @param {string} [sseRoutePath='/events'] - SSE endpoint path.
430
+ * @returns {ServerEngine}
431
+ */
432
+ enableSSE(sseRoutePath = '/events') {
433
+ if (!this.app) throw new Error('Call create() first');
434
+ this.app.get(sseRoutePath, (req, res) => {
435
+ const clientId = `sse_${Date.now()}_${Math.random().toString(36).slice(2)}`;
436
+
437
+ res.writeHead(200, {
438
+ 'Content-Type': 'text/event-stream',
439
+ 'Cache-Control': 'no-cache',
440
+ 'Connection': 'keep-alive',
441
+ 'X-Accel-Buffering': 'no',
442
+ 'Access-Control-Allow-Origin': '*',
443
+ });
444
+
445
+ res.write(`data: ${JSON.stringify({ type: 'connected', clientId })}\n\n`);
446
+
447
+ this._sseClients.set(clientId, res);
448
+
449
+ req.on('close', () => {
450
+ this._sseClients.delete(clientId);
451
+ });
452
+ });
453
+ return this;
454
+ }
455
+
456
+ /**
457
+ * Sends an SSE event to a specific client.
458
+ * @param {string} clientId
459
+ * @param {string} event
460
+ * @param {*} data
461
+ */
462
+ sendSSE(clientId, event, data) {
463
+ const client = this._sseClients.get(clientId);
464
+ if (client) {
465
+ client.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Broadcasts an SSE event to all connected SSE clients.
471
+ * @param {string} event
472
+ * @param {*} data
473
+ */
474
+ broadcastSSE(event, data) {
475
+ this._sseClients.forEach((client) => {
476
+ client.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
477
+ });
478
+ }
479
+
480
+ // ─────────────────────────────────────────────
481
+ // Request Preprocessing
482
+ // ─────────────────────────────────────────────
483
+
484
+ _preprocessRequest(req, res, next) {
485
+ const startMs = Date.now();
486
+ const ip = req.ip || req.connection.remoteAddress || '';
487
+
488
+ // Block listed IPs
489
+ if (this._blockedIPs.has(ip)) {
490
+ res.status(403).json({ error: 'Forbidden', message: 'Your IP has been blocked.' });
491
+ return;
492
+ }
493
+
494
+ // Rate limiting (100 req / 60s per IP by default)
495
+ const rateLimitKey = `preprocess:${ip}`;
496
+ const allowed = this._checkRateLimit(rateLimitKey, 100, 60000);
497
+ if (!allowed) {
498
+ res.status(429).json({ error: 'Too Many Requests', retryAfter: 60 });
499
+ return;
500
+ }
501
+
502
+ // XSS sanitization on query strings and body
503
+ if (req.query) req.query = this._sanitizeObject(req.query);
504
+ if (req.body && typeof req.body === 'object') req.body = this._sanitizeObject(req.body);
505
+
506
+ // SQL injection detection
507
+ const combined = JSON.stringify(req.query) + JSON.stringify(req.body || {});
508
+ if (this._detectSQLInjection(combined)) {
509
+ this.metrics.totalErrors++;
510
+ res.status(400).json({ error: 'Bad Request', message: 'Malicious input detected.' });
511
+ return;
512
+ }
513
+
514
+ // Track metrics
515
+ this.metrics.totalRequests++;
516
+ this.metrics.activeRequests++;
517
+
518
+ res.on('finish', () => {
519
+ const latency = Date.now() - startMs;
520
+ this.metrics.totalLatencyMs += latency;
521
+ this.metrics.activeRequests = Math.max(0, this.metrics.activeRequests - 1);
522
+ const code = String(res.statusCode);
523
+ this.metrics.statusCodes[code] = (this.metrics.statusCodes[code] || 0) + 1;
524
+ if (res.statusCode >= 500) this.metrics.totalErrors++;
525
+ });
526
+
527
+ // Request queue for high-load
528
+ if (this.options.enableRequestQueue) {
529
+ if (this._activeQueuedRequests >= this._queueConcurrency) {
530
+ if (this._requestQueue.length >= this._queueMaxSize) {
531
+ res.status(503).json({ error: 'Service Unavailable', message: 'Server queue is full.' });
532
+ return;
533
+ }
534
+ this._requestQueue.push({ req, res, next, startMs });
535
+ return;
536
+ }
537
+ this._activeQueuedRequests++;
538
+ const origEnd = res.end.bind(res);
539
+ res.end = (...args) => {
540
+ this._activeQueuedRequests--;
541
+ this._processQueue();
542
+ return origEnd(...args);
543
+ };
544
+ }
545
+
546
+ next();
547
+ }
548
+
549
+ _processQueue() {
550
+ if (this._requestQueue.length === 0) return;
551
+ if (this._activeQueuedRequests >= this._queueConcurrency) return;
552
+ const { req, res, next } = this._requestQueue.shift();
553
+ this._activeQueuedRequests++;
554
+ const origEnd = res.end.bind(res);
555
+ res.end = (...args) => {
556
+ this._activeQueuedRequests--;
557
+ this._processQueue();
558
+ return origEnd(...args);
559
+ };
560
+ next();
561
+ }
562
+
563
+ _checkRateLimit(key, maxRequests, windowMs) {
564
+ const now = Date.now();
565
+ const entry = this._rateLimitStore.get(key);
566
+ if (!entry || now > entry.resetAt) {
567
+ this._rateLimitStore.set(key, { count: 1, resetAt: now + windowMs });
568
+ return true;
569
+ }
570
+ if (entry.count >= maxRequests) return false;
571
+ entry.count++;
572
+ return true;
573
+ }
574
+
575
+ _sanitizeObject(obj) {
576
+ if (typeof obj === 'string') return this._sanitizeString(obj);
577
+ if (Array.isArray(obj)) return obj.map((v) => this._sanitizeObject(v));
578
+ if (obj && typeof obj === 'object') {
579
+ const sanitized = {};
580
+ for (const [k, v] of Object.entries(obj)) {
581
+ sanitized[k] = this._sanitizeObject(v);
582
+ }
583
+ return sanitized;
584
+ }
585
+ return obj;
586
+ }
587
+
588
+ _sanitizeString(str) {
589
+ if (typeof str !== 'string') return str;
590
+ return str
591
+ .replace(/&/g, '&amp;')
592
+ .replace(/</g, '&lt;')
593
+ .replace(/>/g, '&gt;')
594
+ .replace(/"/g, '&quot;')
595
+ .replace(/'/g, '&#x27;')
596
+ .replace(/\//g, '&#x2F;');
597
+ }
598
+
599
+ _detectSQLInjection(input) {
600
+ return this._sqlPatterns.some((p) => p.test(input));
601
+ }
602
+
603
+ _errorHandler(err, req, res, next) { // eslint-disable-line no-unused-vars
604
+ this.metrics.totalErrors++;
605
+ const statusCode = err.status || err.statusCode || 500;
606
+ const message = statusCode === 500 ? 'Internal Server Error' : err.message;
607
+ console.error('[ServerEngine] Error:', err.message, err.stack);
608
+ res.status(statusCode).json({
609
+ error: http.STATUS_CODES[statusCode] || 'Error',
610
+ message,
611
+ timestamp: new Date().toISOString(),
612
+ });
613
+ }
614
+
615
+ // ─────────────────────────────────────────────
616
+ // Lifecycle
617
+ // ─────────────────────────────────────────────
618
+
619
+ /**
620
+ * Starts listening on the given port.
621
+ * @param {number} [port] - Overrides the port given in create().
622
+ * @returns {Promise<ServerEngine>}
623
+ */
624
+ start(port) {
625
+ return new Promise((resolve, reject) => {
626
+ if (!this.server) throw new Error('Call create() first');
627
+ const listenPort = port || this.defaultPort || 3000;
628
+ this.server.listen(listenPort, () => {
629
+ this.isRunning = true;
630
+ this.startTime = Date.now();
631
+ this._startMetricsTicker();
632
+ console.log(`[ServerEngine] Server running on port ${listenPort} (PID ${process.pid})`);
633
+ resolve(this);
634
+ });
635
+ this.server.on('error', reject);
636
+ });
637
+ }
638
+
639
+ /**
640
+ * Gracefully shuts down the server.
641
+ * @param {number} [timeout=5000] - Grace period in ms.
642
+ * @returns {Promise<void>}
643
+ */
644
+ stop(timeout = 5000) {
645
+ return new Promise((resolve) => {
646
+ if (!this.server || !this.isRunning) {
647
+ resolve();
648
+ return;
649
+ }
650
+ clearInterval(this._metricsInterval);
651
+ console.log('[ServerEngine] Shutting down gracefully...');
652
+ const timer = setTimeout(() => {
653
+ console.warn('[ServerEngine] Forcing shutdown after timeout.');
654
+ resolve();
655
+ }, timeout);
656
+ this.server.close(() => {
657
+ clearTimeout(timer);
658
+ this.isRunning = false;
659
+ console.log('[ServerEngine] Server stopped.');
660
+ resolve();
661
+ });
662
+ });
663
+ }
664
+
665
+ // ─────────────────────────────────────────────
666
+ // Clustering
667
+ // ─────────────────────────────────────────────
668
+
669
+ /**
670
+ * Starts the server using Node.js cluster module for multi-core usage.
671
+ * Call this INSTEAD of start() when you want clustering.
672
+ * @param {number} [workers] - Number of workers. Defaults to CPU count.
673
+ * @param {number} [port] - Port to listen on.
674
+ * @returns {ServerEngine}
675
+ */
676
+ cluster(workers, port) {
677
+ const numWorkers = workers || os.cpus().length;
678
+ const listenPort = port || this.defaultPort || 3000;
679
+
680
+ if (cluster.isPrimary) {
681
+ console.log(`[ServerEngine] Primary PID ${process.pid} starting ${numWorkers} workers`);
682
+ for (let i = 0; i < numWorkers; i++) {
683
+ cluster.fork();
684
+ }
685
+ cluster.on('exit', (worker, code, signal) => {
686
+ console.warn(`[ServerEngine] Worker ${worker.process.pid} died (${signal || code}). Restarting...`);
687
+ cluster.fork();
688
+ });
689
+ } else {
690
+ this.start(listenPort).catch((err) => {
691
+ console.error('[ServerEngine] Worker failed to start:', err.message);
692
+ process.exit(1);
693
+ });
694
+ }
695
+ return this;
696
+ }
697
+
698
+ // ─────────────────────────────────────────────
699
+ // Metrics Ticker
700
+ // ─────────────────────────────────────────────
701
+
702
+ _startMetricsTicker() {
703
+ let prev = this.metrics.totalRequests;
704
+ this._metricsInterval = setInterval(() => {
705
+ const current = this.metrics.totalRequests;
706
+ this.metrics.requestsPerSecond = (current - prev);
707
+ prev = current;
708
+ }, 1000);
709
+ if (this._metricsInterval.unref) this._metricsInterval.unref();
710
+ }
711
+
712
+ /**
713
+ * Returns current performance metrics snapshot.
714
+ * @returns {object}
715
+ */
716
+ getMetrics() {
717
+ return {
718
+ ...this.metrics,
719
+ uptime: this.startTime ? Date.now() - this.startTime : 0,
720
+ avgLatencyMs: this.metrics.totalRequests > 0
721
+ ? +(this.metrics.totalLatencyMs / this.metrics.totalRequests).toFixed(3)
722
+ : 0,
723
+ queueLength: this._requestQueue.length,
724
+ sseClients: this._sseClients.size,
725
+ wsRooms: this._rooms.size,
726
+ };
727
+ }
728
+
729
+ // ─────────────────────────────────────────────
730
+ // IP Blocking
731
+ // ─────────────────────────────────────────────
732
+
733
+ blockIP(ip) { this._blockedIPs.add(ip); return this; }
734
+ unblockIP(ip) { this._blockedIPs.delete(ip); return this; }
735
+ isBlockedIP(ip) { return this._blockedIPs.has(ip); }
736
+ }
737
+
738
+ module.exports = ServerEngine;