triva 0.0.2 → 0.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 +21 -0
- package/README.md +416 -0
- package/index.d.ts +91 -0
- package/lib/cache.js +112 -0
- package/lib/cookie-parser.js +114 -0
- package/lib/db-adapters.js +580 -0
- package/lib/error-tracker.js +353 -0
- package/lib/index.js +655 -0
- package/lib/log.js +261 -0
- package/lib/middleware.js +237 -0
- package/lib/ua-parser.js +130 -0
- package/package.json +29 -8
package/lib/index.js
ADDED
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Triva
|
|
3
|
+
* Copyright (c) 2026 Kris Powers
|
|
4
|
+
* License MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
import http from 'http';
|
|
10
|
+
import { parse as parseUrl } from 'url';
|
|
11
|
+
import { parse as parseQuery } from 'querystring';
|
|
12
|
+
import { createReadStream, stat } from 'fs';
|
|
13
|
+
import { basename, extname } from 'path';
|
|
14
|
+
import { log } from './log.js';
|
|
15
|
+
import { cache, configCache } from './cache.js';
|
|
16
|
+
import { middleware as createMiddleware } from './middleware.js';
|
|
17
|
+
import { errorTracker } from './error-tracker.js';
|
|
18
|
+
import { cookieParser } from './cookie-parser.js';
|
|
19
|
+
|
|
20
|
+
/* ---------------- Request Context ---------------- */
|
|
21
|
+
class RequestContext {
|
|
22
|
+
constructor(req, res, routeParams = {}) {
|
|
23
|
+
this.req = req;
|
|
24
|
+
this.res = res;
|
|
25
|
+
this.params = routeParams;
|
|
26
|
+
this.query = {};
|
|
27
|
+
this.body = null;
|
|
28
|
+
this.triva = req.triva || {};
|
|
29
|
+
|
|
30
|
+
// Parse query string
|
|
31
|
+
const parsedUrl = parseUrl(req.url, true);
|
|
32
|
+
this.query = parsedUrl.query || {};
|
|
33
|
+
this.pathname = parsedUrl.pathname;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async json() {
|
|
37
|
+
if (this.body !== null) return this.body;
|
|
38
|
+
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
let data = '';
|
|
41
|
+
this.req.on('data', chunk => {
|
|
42
|
+
data += chunk.toString();
|
|
43
|
+
});
|
|
44
|
+
this.req.on('end', () => {
|
|
45
|
+
try {
|
|
46
|
+
this.body = JSON.parse(data);
|
|
47
|
+
resolve(this.body);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
reject(new Error('Invalid JSON'));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
this.req.on('error', reject);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async text() {
|
|
57
|
+
if (this.body !== null) return this.body;
|
|
58
|
+
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
let data = '';
|
|
61
|
+
this.req.on('data', chunk => {
|
|
62
|
+
data += chunk.toString();
|
|
63
|
+
});
|
|
64
|
+
this.req.on('end', () => {
|
|
65
|
+
this.body = data;
|
|
66
|
+
resolve(data);
|
|
67
|
+
});
|
|
68
|
+
this.req.on('error', reject);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* ---------------- Response Helpers ---------------- */
|
|
74
|
+
class ResponseHelpers {
|
|
75
|
+
constructor(res) {
|
|
76
|
+
this.res = res;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
status(code) {
|
|
80
|
+
this.res.statusCode = code;
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
header(name, value) {
|
|
85
|
+
this.res.setHeader(name, value);
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
json(data) {
|
|
90
|
+
if (!this.res.writableEnded) {
|
|
91
|
+
this.res.setHeader('Content-Type', 'application/json');
|
|
92
|
+
this.res.end(JSON.stringify(data));
|
|
93
|
+
}
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
send(data) {
|
|
98
|
+
if (this.res.writableEnded) {
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// If object, send as JSON
|
|
103
|
+
if (typeof data === 'object') {
|
|
104
|
+
return this.json(data);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const stringData = String(data);
|
|
108
|
+
|
|
109
|
+
// Auto-detect HTML content
|
|
110
|
+
if (stringData.trim().startsWith('<') &&
|
|
111
|
+
(stringData.includes('</') || stringData.includes('/>'))) {
|
|
112
|
+
this.res.setHeader('Content-Type', 'text/html');
|
|
113
|
+
} else {
|
|
114
|
+
this.res.setHeader('Content-Type', 'text/plain');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.res.end(stringData);
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
html(data) {
|
|
122
|
+
if (!this.res.writableEnded) {
|
|
123
|
+
this.res.setHeader('Content-Type', 'text/html');
|
|
124
|
+
this.res.end(data);
|
|
125
|
+
}
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
redirect(url, code = 302) {
|
|
130
|
+
this.res.statusCode = code;
|
|
131
|
+
this.res.setHeader('Location', url);
|
|
132
|
+
this.res.end();
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
jsonp(data, callbackParam = 'callback') {
|
|
137
|
+
if (this.res.writableEnded) {
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Get callback name from query parameter
|
|
142
|
+
const parsedUrl = parseUrl(this.res.req?.url || '', true);
|
|
143
|
+
const callback = parsedUrl.query[callbackParam] || 'callback';
|
|
144
|
+
|
|
145
|
+
// Sanitize callback name (only allow safe characters)
|
|
146
|
+
const safeCallback = callback.replace(/[^\[\]\w$.]/g, '');
|
|
147
|
+
|
|
148
|
+
// Create JSONP response
|
|
149
|
+
const jsonString = JSON.stringify(data);
|
|
150
|
+
const body = `/**/ typeof ${safeCallback} === 'function' && ${safeCallback}(${jsonString});`;
|
|
151
|
+
|
|
152
|
+
this.res.setHeader('Content-Type', 'text/javascript; charset=utf-8');
|
|
153
|
+
this.res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
154
|
+
this.res.end(body);
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
download(filepath, filename = null) {
|
|
159
|
+
if (this.res.writableEnded) {
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const downloadName = filename || basename(filepath);
|
|
164
|
+
|
|
165
|
+
stat(filepath, (err, stats) => {
|
|
166
|
+
if (err) {
|
|
167
|
+
this.res.statusCode = 404;
|
|
168
|
+
this.res.end('File not found');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Set headers for download
|
|
173
|
+
this.res.setHeader('Content-Type', 'application/octet-stream');
|
|
174
|
+
this.res.setHeader('Content-Disposition', `attachment; filename="${downloadName}"`);
|
|
175
|
+
this.res.setHeader('Content-Length', stats.size);
|
|
176
|
+
|
|
177
|
+
// Stream file to response
|
|
178
|
+
const fileStream = createReadStream(filepath);
|
|
179
|
+
fileStream.pipe(this.res);
|
|
180
|
+
|
|
181
|
+
fileStream.on('error', (streamErr) => {
|
|
182
|
+
if (!this.res.writableEnded) {
|
|
183
|
+
this.res.statusCode = 500;
|
|
184
|
+
this.res.end('Error reading file');
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return this;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
sendFile(filepath, options = {}) {
|
|
193
|
+
if (this.res.writableEnded) {
|
|
194
|
+
return this;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
stat(filepath, (err, stats) => {
|
|
198
|
+
if (err) {
|
|
199
|
+
this.res.statusCode = 404;
|
|
200
|
+
this.res.end('File not found');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Determine content type from extension
|
|
205
|
+
const ext = extname(filepath).toLowerCase();
|
|
206
|
+
const contentTypes = {
|
|
207
|
+
'.html': 'text/html',
|
|
208
|
+
'.css': 'text/css',
|
|
209
|
+
'.js': 'text/javascript',
|
|
210
|
+
'.json': 'application/json',
|
|
211
|
+
'.png': 'image/png',
|
|
212
|
+
'.jpg': 'image/jpeg',
|
|
213
|
+
'.jpeg': 'image/jpeg',
|
|
214
|
+
'.gif': 'image/gif',
|
|
215
|
+
'.svg': 'image/svg+xml',
|
|
216
|
+
'.pdf': 'application/pdf',
|
|
217
|
+
'.txt': 'text/plain',
|
|
218
|
+
'.xml': 'application/xml'
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const contentType = options.contentType || contentTypes[ext] || 'application/octet-stream';
|
|
222
|
+
|
|
223
|
+
// Set headers
|
|
224
|
+
this.res.setHeader('Content-Type', contentType);
|
|
225
|
+
this.res.setHeader('Content-Length', stats.size);
|
|
226
|
+
|
|
227
|
+
if (options.headers) {
|
|
228
|
+
Object.keys(options.headers).forEach(key => {
|
|
229
|
+
this.res.setHeader(key, options.headers[key]);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Stream file to response
|
|
234
|
+
const fileStream = createReadStream(filepath);
|
|
235
|
+
fileStream.pipe(this.res);
|
|
236
|
+
|
|
237
|
+
fileStream.on('error', (streamErr) => {
|
|
238
|
+
if (!this.res.writableEnded) {
|
|
239
|
+
this.res.statusCode = 500;
|
|
240
|
+
this.res.end('Error reading file');
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return this;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
end(data) {
|
|
249
|
+
if (!this.res.writableEnded) {
|
|
250
|
+
this.res.end(data);
|
|
251
|
+
}
|
|
252
|
+
return this;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* ---------------- Route Matcher ---------------- */
|
|
257
|
+
class RouteMatcher {
|
|
258
|
+
constructor() {
|
|
259
|
+
this.routes = {
|
|
260
|
+
GET: [],
|
|
261
|
+
POST: [],
|
|
262
|
+
PUT: [],
|
|
263
|
+
DELETE: [],
|
|
264
|
+
PATCH: [],
|
|
265
|
+
HEAD: [],
|
|
266
|
+
OPTIONS: []
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
_parsePattern(pattern) {
|
|
271
|
+
const paramNames = [];
|
|
272
|
+
const regexPattern = pattern
|
|
273
|
+
.split('/')
|
|
274
|
+
.map(segment => {
|
|
275
|
+
if (segment.startsWith(':')) {
|
|
276
|
+
paramNames.push(segment.slice(1));
|
|
277
|
+
return '([^/]+)';
|
|
278
|
+
}
|
|
279
|
+
if (segment === '*') {
|
|
280
|
+
return '.*';
|
|
281
|
+
}
|
|
282
|
+
return segment;
|
|
283
|
+
})
|
|
284
|
+
.join('/');
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
regex: new RegExp(`^${regexPattern}$`),
|
|
288
|
+
paramNames
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
addRoute(method, pattern, handler) {
|
|
293
|
+
const parsed = this._parsePattern(pattern);
|
|
294
|
+
this.routes[method.toUpperCase()].push({
|
|
295
|
+
pattern,
|
|
296
|
+
...parsed,
|
|
297
|
+
handler
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
match(method, pathname) {
|
|
302
|
+
const routes = this.routes[method.toUpperCase()] || [];
|
|
303
|
+
|
|
304
|
+
for (const route of routes) {
|
|
305
|
+
const match = pathname.match(route.regex);
|
|
306
|
+
if (match) {
|
|
307
|
+
const params = {};
|
|
308
|
+
route.paramNames.forEach((name, i) => {
|
|
309
|
+
params[name] = match[i + 1];
|
|
310
|
+
});
|
|
311
|
+
return { handler: route.handler, params };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* ---------------- Server Core ---------------- */
|
|
320
|
+
class TrivaServer {
|
|
321
|
+
constructor(options = {}) {
|
|
322
|
+
this.options = {
|
|
323
|
+
env: options.env || 'production',
|
|
324
|
+
...options
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
this.server = null;
|
|
328
|
+
this.router = new RouteMatcher();
|
|
329
|
+
this.middlewareStack = [];
|
|
330
|
+
this.errorHandler = this._defaultErrorHandler.bind(this);
|
|
331
|
+
this.notFoundHandler = this._defaultNotFoundHandler.bind(this);
|
|
332
|
+
|
|
333
|
+
// Bind routing methods
|
|
334
|
+
this.get = this.get.bind(this);
|
|
335
|
+
this.post = this.post.bind(this);
|
|
336
|
+
this.put = this.put.bind(this);
|
|
337
|
+
this.delete = this.delete.bind(this);
|
|
338
|
+
this.patch = this.patch.bind(this);
|
|
339
|
+
this.use = this.use.bind(this);
|
|
340
|
+
this.listen = this.listen.bind(this);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
use(middleware) {
|
|
344
|
+
if (typeof middleware !== 'function') {
|
|
345
|
+
throw new Error('Middleware must be a function');
|
|
346
|
+
}
|
|
347
|
+
this.middlewareStack.push(middleware);
|
|
348
|
+
return this;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
get(pattern, handler) {
|
|
352
|
+
this.router.addRoute('GET', pattern, handler);
|
|
353
|
+
return this;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
post(pattern, handler) {
|
|
357
|
+
this.router.addRoute('POST', pattern, handler);
|
|
358
|
+
return this;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
put(pattern, handler) {
|
|
362
|
+
this.router.addRoute('PUT', pattern, handler);
|
|
363
|
+
return this;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
delete(pattern, handler) {
|
|
367
|
+
this.router.addRoute('DELETE', pattern, handler);
|
|
368
|
+
return this;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
patch(pattern, handler) {
|
|
372
|
+
this.router.addRoute('PATCH', pattern, handler);
|
|
373
|
+
return this;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
setErrorHandler(handler) {
|
|
377
|
+
this.errorHandler = handler;
|
|
378
|
+
return this;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
setNotFoundHandler(handler) {
|
|
382
|
+
this.notFoundHandler = handler;
|
|
383
|
+
return this;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async _runMiddleware(req, res) {
|
|
387
|
+
for (const middleware of this.middlewareStack) {
|
|
388
|
+
try {
|
|
389
|
+
await new Promise((resolve, reject) => {
|
|
390
|
+
try {
|
|
391
|
+
middleware(req, res, (err) => {
|
|
392
|
+
if (err) reject(err);
|
|
393
|
+
else resolve();
|
|
394
|
+
});
|
|
395
|
+
} catch (err) {
|
|
396
|
+
reject(err);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
} catch (err) {
|
|
400
|
+
// Capture middleware errors with context
|
|
401
|
+
await errorTracker.capture(err, {
|
|
402
|
+
req,
|
|
403
|
+
phase: 'middleware',
|
|
404
|
+
handler: middleware.name || 'anonymous',
|
|
405
|
+
pathname: parseUrl(req.url).pathname,
|
|
406
|
+
uaData: req.triva?.throttle?.uaData
|
|
407
|
+
});
|
|
408
|
+
throw err; // Re-throw to be handled by main error handler
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async _handleRequest(req, res) {
|
|
414
|
+
try {
|
|
415
|
+
// Enhance request and response objects
|
|
416
|
+
req.triva = req.triva || {};
|
|
417
|
+
|
|
418
|
+
// Store req reference in res for methods that need it (like jsonp)
|
|
419
|
+
res.req = req;
|
|
420
|
+
|
|
421
|
+
// Add helper methods directly to res object
|
|
422
|
+
const helpers = new ResponseHelpers(res);
|
|
423
|
+
res.status = helpers.status.bind(helpers);
|
|
424
|
+
res.header = helpers.header.bind(helpers);
|
|
425
|
+
res.json = helpers.json.bind(helpers);
|
|
426
|
+
res.send = helpers.send.bind(helpers);
|
|
427
|
+
res.html = helpers.html.bind(helpers);
|
|
428
|
+
res.redirect = helpers.redirect.bind(helpers);
|
|
429
|
+
res.jsonp = helpers.jsonp.bind(helpers);
|
|
430
|
+
res.download = helpers.download.bind(helpers);
|
|
431
|
+
res.sendFile = helpers.sendFile.bind(helpers);
|
|
432
|
+
|
|
433
|
+
// Run middleware stack
|
|
434
|
+
await this._runMiddleware(req, res);
|
|
435
|
+
|
|
436
|
+
// Check if response was already sent by middleware
|
|
437
|
+
if (res.writableEnded) return;
|
|
438
|
+
|
|
439
|
+
const parsedUrl = parseUrl(req.url, true);
|
|
440
|
+
const pathname = parsedUrl.pathname;
|
|
441
|
+
|
|
442
|
+
// Route matching
|
|
443
|
+
const match = this.router.match(req.method, pathname);
|
|
444
|
+
|
|
445
|
+
if (!match) {
|
|
446
|
+
return this.notFoundHandler(req, res);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Create context
|
|
450
|
+
const context = new RequestContext(req, res, match.params);
|
|
451
|
+
|
|
452
|
+
// Execute route handler with error tracking
|
|
453
|
+
try {
|
|
454
|
+
await match.handler(context, res);
|
|
455
|
+
} catch (handlerError) {
|
|
456
|
+
// Capture route handler errors
|
|
457
|
+
await errorTracker.capture(handlerError, {
|
|
458
|
+
req,
|
|
459
|
+
phase: 'route',
|
|
460
|
+
route: pathname,
|
|
461
|
+
handler: 'route_handler',
|
|
462
|
+
pathname,
|
|
463
|
+
uaData: req.triva?.throttle?.uaData
|
|
464
|
+
});
|
|
465
|
+
throw handlerError;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
} catch (err) {
|
|
469
|
+
// Capture top-level request errors if not already captured
|
|
470
|
+
if (!err._trivaTracked) {
|
|
471
|
+
await errorTracker.capture(err, {
|
|
472
|
+
req,
|
|
473
|
+
phase: 'request',
|
|
474
|
+
pathname: parseUrl(req.url).pathname,
|
|
475
|
+
uaData: req.triva?.throttle?.uaData
|
|
476
|
+
});
|
|
477
|
+
err._trivaTracked = true; // Mark to avoid double-tracking
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
this.errorHandler(err, req, res);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
_defaultErrorHandler(err, req, res) {
|
|
485
|
+
console.error('Server Error:', err);
|
|
486
|
+
|
|
487
|
+
if (!res.writableEnded) {
|
|
488
|
+
res.statusCode = 500;
|
|
489
|
+
res.setHeader('Content-Type', 'application/json');
|
|
490
|
+
|
|
491
|
+
const response = {
|
|
492
|
+
error: 'Internal Server Error',
|
|
493
|
+
message: this.options.env === 'development' ? err.message : undefined,
|
|
494
|
+
stack: this.options.env === 'development' ? err.stack : undefined
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
res.end(JSON.stringify(response));
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
_defaultNotFoundHandler(req, res) {
|
|
502
|
+
if (!res.writableEnded) {
|
|
503
|
+
res.statusCode = 404;
|
|
504
|
+
res.setHeader('Content-Type', 'application/json');
|
|
505
|
+
res.end(JSON.stringify({
|
|
506
|
+
error: 'Not Found',
|
|
507
|
+
path: req.url
|
|
508
|
+
}));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
listen(port, callback) {
|
|
513
|
+
this.server = http.createServer((req, res) => {
|
|
514
|
+
this._handleRequest(req, res);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
this.server.listen(port, () => {
|
|
518
|
+
console.log(`Triva server listening on port ${port} (${this.options.env})`);
|
|
519
|
+
if (callback) callback();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
return this.server;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
close(callback) {
|
|
526
|
+
if (this.server) {
|
|
527
|
+
this.server.close(callback);
|
|
528
|
+
}
|
|
529
|
+
return this;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/* ---------------- Factory & Exports ---------------- */
|
|
534
|
+
let globalServer = null;
|
|
535
|
+
|
|
536
|
+
async function build(options = {}) {
|
|
537
|
+
globalServer = new TrivaServer(options);
|
|
538
|
+
|
|
539
|
+
// Centralized configuration
|
|
540
|
+
if (options.cache) {
|
|
541
|
+
await configCache(options.cache);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (options.middleware || options.throttle || options.retention) {
|
|
545
|
+
const middlewareOptions = {
|
|
546
|
+
...options.middleware,
|
|
547
|
+
throttle: options.throttle || options.middleware?.throttle,
|
|
548
|
+
retention: options.retention || options.middleware?.retention
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
if (middlewareOptions.throttle || middlewareOptions.retention) {
|
|
552
|
+
const mw = createMiddleware(middlewareOptions);
|
|
553
|
+
globalServer.use(mw);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (options.errorTracking) {
|
|
558
|
+
errorTracker.configure(options.errorTracking);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return globalServer;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function middleware(options = {}) {
|
|
565
|
+
if (!globalServer) {
|
|
566
|
+
throw new Error('Server not initialized. Call build() first.');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const mw = createMiddleware(options);
|
|
570
|
+
globalServer.use(mw);
|
|
571
|
+
return globalServer;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function get(pattern, handler) {
|
|
575
|
+
if (!globalServer) {
|
|
576
|
+
throw new Error('Server not initialized. Call build() first.');
|
|
577
|
+
}
|
|
578
|
+
return globalServer.get(pattern, handler);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function post(pattern, handler) {
|
|
582
|
+
if (!globalServer) {
|
|
583
|
+
throw new Error('Server not initialized. Call build() first.');
|
|
584
|
+
}
|
|
585
|
+
return globalServer.post(pattern, handler);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function put(pattern, handler) {
|
|
589
|
+
if (!globalServer) {
|
|
590
|
+
throw new Error('Server not initialized. Call build() first.');
|
|
591
|
+
}
|
|
592
|
+
return globalServer.put(pattern, handler);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function del(pattern, handler) {
|
|
596
|
+
if (!globalServer) {
|
|
597
|
+
throw new Error('Server not initialized. Call build() first.');
|
|
598
|
+
}
|
|
599
|
+
return globalServer.delete(pattern, handler);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function patch(pattern, handler) {
|
|
603
|
+
if (!globalServer) {
|
|
604
|
+
throw new Error('Server not initialized. Call build() first.');
|
|
605
|
+
}
|
|
606
|
+
return globalServer.patch(pattern, handler);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function use(middleware) {
|
|
610
|
+
if (!globalServer) {
|
|
611
|
+
throw new Error('Server not initialized. Call build() first.');
|
|
612
|
+
}
|
|
613
|
+
return globalServer.use(middleware);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function listen(port, callback) {
|
|
617
|
+
if (!globalServer) {
|
|
618
|
+
throw new Error('Server not initialized. Call build() first.');
|
|
619
|
+
}
|
|
620
|
+
return globalServer.listen(port, callback);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function setErrorHandler(handler) {
|
|
624
|
+
if (!globalServer) {
|
|
625
|
+
throw new Error('Server not initialized. Call build() first.');
|
|
626
|
+
}
|
|
627
|
+
return globalServer.setErrorHandler(handler);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function setNotFoundHandler(handler) {
|
|
631
|
+
if (!globalServer) {
|
|
632
|
+
throw new Error('Server not initialized. Call build() first.');
|
|
633
|
+
}
|
|
634
|
+
return globalServer.setNotFoundHandler(handler);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export {
|
|
638
|
+
build,
|
|
639
|
+
middleware,
|
|
640
|
+
get,
|
|
641
|
+
post,
|
|
642
|
+
put,
|
|
643
|
+
del as delete,
|
|
644
|
+
patch,
|
|
645
|
+
use,
|
|
646
|
+
listen,
|
|
647
|
+
setErrorHandler,
|
|
648
|
+
setNotFoundHandler,
|
|
649
|
+
TrivaServer,
|
|
650
|
+
log,
|
|
651
|
+
cache,
|
|
652
|
+
configCache,
|
|
653
|
+
errorTracker,
|
|
654
|
+
cookieParser
|
|
655
|
+
};
|