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.
- package/bin/thy-node.js +332 -0
- package/bin/thy.js +828 -0
- package/bin/thys.js +25 -0
- package/examples/calculator.thy +274 -0
- package/examples/calculator.thy.js +628 -0
- package/examples/hello.thy +34 -0
- package/examples/login.thy +382 -0
- package/examples/login.thy.js +710 -0
- package/package.json +54 -0
- package/scripts/_class.js +18 -0
- package/scripts/_class2.js +11 -0
- package/scripts/_class3.js +21 -0
- package/scripts/_dstruct.js +20 -0
- package/scripts/_dstruct2.js +24 -0
- package/scripts/_scaffold.js +16 -0
- package/scripts/_scaffold2.js +16 -0
- package/scripts/_scaffold3.js +26 -0
- package/scripts/_tpl_compile.js +13 -0
- package/scripts/_tpl_test.js +9 -0
- package/scripts/_typeof.js +26 -0
- package/scripts/_typeof2.js +25 -0
- package/scripts/patch_genmethod.js +44 -0
- package/scripts/smoke_test.js +167 -0
- package/scripts/smoke_test2.js +151 -0
- package/scripts/test_control_panel.js +58 -0
- package/src/compiler/CodeGenerator.js +1449 -0
- package/src/compiler/JITCompiler.js +325 -0
- package/src/database/DatabaseEngine.js +931 -0
- package/src/errors/ErrorDetector.js +305 -0
- package/src/errors/ErrorMessages.js +497 -0
- package/src/index.js +169 -0
- package/src/lexer/Lexer.js +342 -0
- package/src/lexer/TokenTypes.js +1234 -0
- package/src/memory/MemoryManager.js +283 -0
- package/src/parser/AST.js +606 -0
- package/src/parser/Parser.js +1942 -0
- package/src/runtime/ThyRuntime.js +265 -0
- package/src/runtime/stdlib/ArrayLib.js +207 -0
- package/src/runtime/stdlib/FileLib.js +168 -0
- package/src/runtime/stdlib/MathLib.js +411 -0
- package/src/runtime/stdlib/StringLib.js +387 -0
- package/src/server/ControlPanel.js +1188 -0
- package/src/server/HTMLCSSEngine.js +866 -0
- package/src/server/SecurityEngine.js +714 -0
- package/src/server/ServerEngine.js +738 -0
- package/tests/test1_math.thy +82 -0
- package/tests/test2_oop.thy +133 -0
- package/tests/test3_server.thy +143 -0
- 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, '&')
|
|
592
|
+
.replace(/</g, '<')
|
|
593
|
+
.replace(/>/g, '>')
|
|
594
|
+
.replace(/"/g, '"')
|
|
595
|
+
.replace(/'/g, ''')
|
|
596
|
+
.replace(/\//g, '/');
|
|
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;
|