te.js 2.2.0 → 2.2.1
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/README.md +1 -12
- package/docs/README.md +1 -2
- package/docs/api-reference.md +124 -186
- package/docs/configuration.md +0 -13
- package/docs/getting-started.md +19 -21
- package/docs/rate-limiting.md +59 -58
- package/lib/llm/client.js +1 -1
- package/package.json +1 -1
- package/radar/index.js +191 -90
- package/rate-limit/base.js +12 -15
- package/rate-limit/index.js +12 -12
- package/rate-limit/index.test.js +45 -16
- package/rate-limit/storage/memory.js +13 -13
- package/rate-limit/storage/redis-install.js +70 -0
- package/rate-limit/storage/redis.js +94 -52
- package/server/ammo.js +3 -2
- package/te.js +64 -143
- package/utils/errors-llm-config.js +63 -0
- package/utils/startup.js +80 -0
- package/database/index.js +0 -167
- package/database/mongodb.js +0 -152
- package/database/redis.js +0 -210
- package/docs/database.md +0 -390
package/te.js
CHANGED
|
@@ -6,7 +6,6 @@ import corsMiddleware from './cors/index.js';
|
|
|
6
6
|
import radarMiddleware from './radar/index.js';
|
|
7
7
|
|
|
8
8
|
import targetRegistry from './server/targets/registry.js';
|
|
9
|
-
import dbManager from './database/index.js';
|
|
10
9
|
|
|
11
10
|
import { loadConfigFile, standardizeObj } from './utils/configuration.js';
|
|
12
11
|
|
|
@@ -14,10 +13,12 @@ import targetHandler from './server/handler.js';
|
|
|
14
13
|
import {
|
|
15
14
|
getErrorsLlmConfig,
|
|
16
15
|
validateErrorsLlmAtTakeoff,
|
|
16
|
+
verifyLlmConnection,
|
|
17
17
|
} from './utils/errors-llm-config.js';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { pathToFileURL } from 'node:url';
|
|
20
20
|
import { readFile } from 'node:fs/promises';
|
|
21
|
+
import { readFrameworkVersion, fmtMs, statusLine } from './utils/startup.js';
|
|
21
22
|
import { findTargetFiles } from './utils/auto-register.js';
|
|
22
23
|
import { registerDocRoutes } from './auto-docs/ui/docs-ui.js';
|
|
23
24
|
import TejError from './server/error.js';
|
|
@@ -26,7 +27,8 @@ const logger = new TejLogger('Tejas');
|
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Performs a graceful shutdown: closes the HTTP server (if started), then exits.
|
|
29
|
-
* Invoked by process-level fatal error handlers.
|
|
30
|
+
* Invoked by process-level fatal error handlers. This is used to ensure that the server is closed properly
|
|
31
|
+
* when the process is terminated.
|
|
30
32
|
* @param {number} [exitCode=1]
|
|
31
33
|
*/
|
|
32
34
|
async function gracefulShutdown(exitCode = 1) {
|
|
@@ -70,7 +72,7 @@ process.on('uncaughtException', (error) => {
|
|
|
70
72
|
* @class
|
|
71
73
|
* @description
|
|
72
74
|
* Tejas is a Node.js framework for building powerful backend services.
|
|
73
|
-
* It provides features like routing, middleware support,
|
|
75
|
+
* It provides features like routing, middleware support,
|
|
74
76
|
* and automatic target (route) registration.
|
|
75
77
|
*/
|
|
76
78
|
class Tejas {
|
|
@@ -83,13 +85,6 @@ class Tejas {
|
|
|
83
85
|
* @param {boolean} [args.log.http_requests] - Whether to log incoming HTTP requests
|
|
84
86
|
* @param {boolean} [args.log.exceptions] - Whether to log exceptions
|
|
85
87
|
* @param {Object} [args.db] - Database configuration options
|
|
86
|
-
* @param {Object} [args.withRedis] - Redis connection configuration
|
|
87
|
-
* @param {boolean} [args.withRedis.isCluster=false] - Whether to use Redis Cluster
|
|
88
|
-
* @param {Object} [args.withRedis.socket] - Redis socket connection options
|
|
89
|
-
* @param {string} [args.withRedis.socket.host] - Redis server hostname
|
|
90
|
-
* @param {number} [args.withRedis.socket.port] - Redis server port
|
|
91
|
-
* @param {boolean} [args.withRedis.socket.tls] - Whether to use TLS for connection
|
|
92
|
-
* @param {string} [args.withRedis.url] - Redis connection URL (alternative to socket config)
|
|
93
88
|
*/
|
|
94
89
|
constructor(args) {
|
|
95
90
|
if (Tejas.instance) return Tejas.instance;
|
|
@@ -203,153 +198,70 @@ class Tejas {
|
|
|
203
198
|
/**
|
|
204
199
|
* Starts the Tejas server
|
|
205
200
|
*
|
|
206
|
-
* @param {Object} [options] - Server configuration options
|
|
207
|
-
* @param {Object} [options.withRedis] - Redis connection options
|
|
208
|
-
* @param {Object} [options.withMongo] - MongoDB connection options (https://www.mongodb.com/docs/drivers/node/current/fundamentals/connection/)
|
|
209
201
|
* @description
|
|
210
202
|
* Creates and starts an HTTP server on the configured port.
|
|
211
|
-
* Optionally initializes Redis and/or MongoDB connections if configuration is provided.
|
|
212
|
-
* For Redis, accepts cluster flag and all connection options supported by node-redis package.
|
|
213
|
-
* For MongoDB, accepts all connection options supported by the official MongoDB Node.js driver.
|
|
214
203
|
*
|
|
215
204
|
* @example
|
|
216
205
|
* const app = new Tejas();
|
|
217
|
-
*
|
|
218
|
-
* // Start server with Redis and MongoDB
|
|
219
|
-
* app.takeoff({
|
|
220
|
-
* withRedis: {
|
|
221
|
-
* url: 'redis://alice:foobared@awesome.redis.server:6380',
|
|
222
|
-
* isCluster: false
|
|
223
|
-
* },
|
|
224
|
-
* withMongo: { url: 'mongodb://localhost:27017/mydatabase' }
|
|
225
|
-
* });
|
|
226
|
-
*
|
|
227
|
-
* // Start server with only Redis using defaults
|
|
228
|
-
* app.takeoff({
|
|
229
|
-
* withRedis: { url: 'redis://localhost:6379' }
|
|
230
|
-
* });
|
|
231
|
-
*
|
|
232
|
-
* // Start server without databases
|
|
233
206
|
* app.takeoff(); // Server starts on default port 1403
|
|
234
207
|
*/
|
|
235
|
-
async takeoff(
|
|
208
|
+
async takeoff() {
|
|
209
|
+
const t0 = Date.now();
|
|
210
|
+
|
|
236
211
|
// Load configuration first (async file read)
|
|
237
212
|
await this.generateConfiguration();
|
|
238
213
|
|
|
214
|
+
// ── Startup banner ──────────────────────────────────────────────────
|
|
215
|
+
const version = await readFrameworkVersion();
|
|
216
|
+
const port = env('PORT');
|
|
217
|
+
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
218
|
+
const banner = [
|
|
219
|
+
'',
|
|
220
|
+
' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
|
|
221
|
+
` Tejas v${version}`,
|
|
222
|
+
` Port: ${port}`,
|
|
223
|
+
` PID: ${process.pid}`,
|
|
224
|
+
` Node: ${process.version}`,
|
|
225
|
+
` Env: ${nodeEnv}`,
|
|
226
|
+
' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
|
|
227
|
+
'',
|
|
228
|
+
].join('\n');
|
|
229
|
+
process.stdout.write(banner + '\n');
|
|
230
|
+
|
|
231
|
+
// ── Live feature status ────────────────────────────────────────────
|
|
232
|
+
const line = statusLine(process.stdout.isTTY);
|
|
233
|
+
|
|
234
|
+
if (this._radarStatus) {
|
|
235
|
+
const s = this._radarStatus;
|
|
236
|
+
line.finish(s.feature, s.ok, s.detail);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
239
|
validateErrorsLlmAtTakeoff();
|
|
240
240
|
const errorsLlm = getErrorsLlmConfig();
|
|
241
241
|
if (errorsLlm.enabled) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
242
|
+
if (errorsLlm.verifyOnStart) {
|
|
243
|
+
line.start('LLM Errors', 'verifying model...');
|
|
244
|
+
const result = await verifyLlmConnection();
|
|
245
|
+
line.finish('LLM Errors', result.status.ok, result.status.detail);
|
|
246
|
+
} else {
|
|
247
|
+
line.finish(
|
|
248
|
+
'LLM Errors',
|
|
249
|
+
true,
|
|
250
|
+
`enabled (${errorsLlm.model || 'default model'}, mode: ${errorsLlm.mode})`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
245
253
|
}
|
|
246
254
|
|
|
247
|
-
// Register target files before the server starts listening so no request
|
|
248
|
-
// can arrive before all routes are fully registered.
|
|
249
255
|
await this.registerTargetsDir();
|
|
250
256
|
|
|
257
|
+
// ── Start HTTP server ───────────────────────────────────────────────
|
|
251
258
|
this.engine = createServer(targetHandler);
|
|
252
|
-
this.engine.listen(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (withRedis) await this.withRedis(withRedis);
|
|
256
|
-
if (withMongo) await this.withMongo(withMongo);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
this.engine.on('error', (err) => {
|
|
260
|
-
logger.error(`Server error: ${err}`);
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Initializes a Redis connection
|
|
266
|
-
*
|
|
267
|
-
* @param {Object} [config] - Redis connection configuration
|
|
268
|
-
* @param {boolean} [config.isCluster=false] - Whether to use Redis Cluster
|
|
269
|
-
* @param {Object} [config.socket] - Redis socket connection options
|
|
270
|
-
* @param {string} [config.socket.host] - Redis server hostname
|
|
271
|
-
* @param {number} [config.socket.port] - Redis server port
|
|
272
|
-
* @param {boolean} [config.socket.tls] - Whether to use TLS for connection
|
|
273
|
-
* @param {string} [config.url] - Redis connection URL (alternative to socket config)
|
|
274
|
-
* @returns {Promise<Tejas>} Returns a Promise that resolves to this instance for chaining
|
|
275
|
-
*
|
|
276
|
-
* @example
|
|
277
|
-
* // Initialize Redis with URL
|
|
278
|
-
* await app.withRedis({
|
|
279
|
-
* url: 'redis://localhost:6379'
|
|
280
|
-
* }).withRateLimit({
|
|
281
|
-
* maxRequests: 100,
|
|
282
|
-
* store: 'redis'
|
|
283
|
-
* });
|
|
284
|
-
*
|
|
285
|
-
* @example
|
|
286
|
-
* // Initialize Redis with socket options
|
|
287
|
-
* await app.withRedis({
|
|
288
|
-
* socket: {
|
|
289
|
-
* host: 'localhost',
|
|
290
|
-
* port: 6379
|
|
291
|
-
* }
|
|
292
|
-
* });
|
|
293
|
-
*/
|
|
294
|
-
async withRedis(config) {
|
|
295
|
-
if (config) {
|
|
296
|
-
await dbManager.initializeConnection('redis', config);
|
|
297
|
-
} else {
|
|
298
|
-
logger.warn(
|
|
299
|
-
'No Redis configuration provided. Skipping Redis connection.',
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return this;
|
|
304
|
-
}
|
|
259
|
+
await new Promise((resolve) => this.engine.listen(port, resolve));
|
|
260
|
+
this.engine.on('error', (err) => logger.error(`Server error: ${err}`));
|
|
305
261
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
* @param {Object} [config] - MongoDB connection configuration
|
|
310
|
-
* @param {string} [config.uri] - MongoDB connection URI
|
|
311
|
-
* @param {Object} [config.options] - Additional MongoDB connection options
|
|
312
|
-
* @returns {Tejas} Returns a Promise that resolves to this instance for chaining
|
|
313
|
-
*
|
|
314
|
-
* @example
|
|
315
|
-
* // Initialize MongoDB with URI
|
|
316
|
-
* await app.withMongo({
|
|
317
|
-
* uri: 'mongodb://localhost:27017/myapp'
|
|
318
|
-
* });
|
|
319
|
-
*
|
|
320
|
-
* @example
|
|
321
|
-
* // Initialize MongoDB with options
|
|
322
|
-
* await app.withMongo({
|
|
323
|
-
* uri: 'mongodb://localhost:27017/myapp',
|
|
324
|
-
* options: {
|
|
325
|
-
* useNewUrlParser: true,
|
|
326
|
-
* useUnifiedTopology: true
|
|
327
|
-
* }
|
|
328
|
-
* });
|
|
329
|
-
*
|
|
330
|
-
* @example
|
|
331
|
-
* // Chain database connections
|
|
332
|
-
* await app
|
|
333
|
-
* .withMongo({
|
|
334
|
-
* uri: 'mongodb://localhost:27017/myapp'
|
|
335
|
-
* })
|
|
336
|
-
* .withRedis({
|
|
337
|
-
* url: 'redis://localhost:6379'
|
|
338
|
-
* })
|
|
339
|
-
* .withRateLimit({
|
|
340
|
-
* maxRequests: 100,
|
|
341
|
-
* store: 'redis'
|
|
342
|
-
* });
|
|
343
|
-
*/
|
|
344
|
-
withMongo(config) {
|
|
345
|
-
if (config) {
|
|
346
|
-
dbManager.initializeConnection('mongodb', config);
|
|
347
|
-
} else {
|
|
348
|
-
logger.warn(
|
|
349
|
-
'No MongoDB configuration provided. Skipping MongoDB connection.',
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
return this;
|
|
262
|
+
process.stdout.write(
|
|
263
|
+
`\n \x1b[32m\u2708 Ready on port ${port} in ${fmtMs(Date.now() - t0)}\x1b[0m\n\n`,
|
|
264
|
+
);
|
|
353
265
|
}
|
|
354
266
|
|
|
355
267
|
/**
|
|
@@ -369,6 +281,7 @@ class Tejas {
|
|
|
369
281
|
* @param {number} [config.rateLimit] - Max LLM calls per minute across all requests (default 10)
|
|
370
282
|
* @param {boolean} [config.cache] - Cache LLM results by throw site + error message to avoid repeated calls (default true)
|
|
371
283
|
* @param {number} [config.cacheTTL] - How long cached results are reused in milliseconds (default 3600000 = 1 hour)
|
|
284
|
+
* @param {boolean} [config.verifyOnStart] - Send a test prompt to the LLM at startup to verify connectivity (default false)
|
|
372
285
|
* @returns {Tejas} The Tejas instance for chaining
|
|
373
286
|
*
|
|
374
287
|
* @example
|
|
@@ -400,6 +313,8 @@ class Tejas {
|
|
|
400
313
|
if (config.cache != null) setEnv('ERRORS_LLM_CACHE', config.cache);
|
|
401
314
|
if (config.cacheTTL != null)
|
|
402
315
|
setEnv('ERRORS_LLM_CACHE_TTL', config.cacheTTL);
|
|
316
|
+
if (config.verifyOnStart != null)
|
|
317
|
+
setEnv('ERRORS_LLM_VERIFY_ON_START', config.verifyOnStart);
|
|
403
318
|
}
|
|
404
319
|
return this;
|
|
405
320
|
}
|
|
@@ -411,8 +326,10 @@ class Tejas {
|
|
|
411
326
|
* @param {number} [config.maxRequests=60] - Maximum number of requests allowed in the time window
|
|
412
327
|
* @param {number} [config.timeWindowSeconds=60] - Time window in seconds
|
|
413
328
|
* @param {string} [config.algorithm='sliding-window'] - Rate-limiting algorithm ('token-bucket', 'sliding-window', or 'fixed-window')
|
|
329
|
+
* @param {string|Object} [config.store='memory'] - Storage backend: 'memory' (default) or
|
|
330
|
+
* { type: 'redis', url: 'redis://...', ...redisOptions } for distributed deployments.
|
|
331
|
+
* In-memory storage is not shared across processes and may be inaccurate in distributed setups.
|
|
414
332
|
* @param {Object} [config.algorithmOptions] - Algorithm-specific options
|
|
415
|
-
* @param {Object} [config.redis] - Redis configuration for distributed rate limiting
|
|
416
333
|
* @param {Function} [config.keyGenerator] - Function to generate unique identifiers (defaults to IP-based)
|
|
417
334
|
* @param {Object} [config.headerFormat] - Rate limit header format configuration
|
|
418
335
|
* @returns {Tejas} The Tejas instance for chaining
|
|
@@ -498,10 +415,10 @@ class Tejas {
|
|
|
498
415
|
* Note: the collector enforces its own non-bypassable masking
|
|
499
416
|
* layer server-side regardless of this setting.
|
|
500
417
|
*
|
|
501
|
-
* @returns {Tejas} The Tejas instance for chaining
|
|
418
|
+
* @returns {Promise<Tejas>} The Tejas instance for chaining
|
|
502
419
|
*
|
|
503
420
|
* @example
|
|
504
|
-
* app.withRadar({ apiKey: process.env.RADAR_API_KEY });
|
|
421
|
+
* await app.withRadar({ apiKey: process.env.RADAR_API_KEY });
|
|
505
422
|
* app.takeoff();
|
|
506
423
|
*
|
|
507
424
|
* @example
|
|
@@ -526,8 +443,12 @@ class Tejas {
|
|
|
526
443
|
* },
|
|
527
444
|
* });
|
|
528
445
|
*/
|
|
529
|
-
withRadar(config = {}) {
|
|
530
|
-
|
|
446
|
+
async withRadar(config = {}) {
|
|
447
|
+
const mw = await radarMiddleware(config);
|
|
448
|
+
if (mw._radarStatus) {
|
|
449
|
+
this._radarStatus = mw._radarStatus;
|
|
450
|
+
}
|
|
451
|
+
this.midair(mw);
|
|
531
452
|
return this;
|
|
532
453
|
}
|
|
533
454
|
|
|
@@ -68,6 +68,7 @@ function normalizeChannel(v) {
|
|
|
68
68
|
* rateLimit: number,
|
|
69
69
|
* cache: boolean,
|
|
70
70
|
* cacheTTL: number,
|
|
71
|
+
* verifyOnStart: boolean,
|
|
71
72
|
* }}
|
|
72
73
|
*/
|
|
73
74
|
export function getErrorsLlmConfig() {
|
|
@@ -137,6 +138,13 @@ export function getErrorsLlmConfig() {
|
|
|
137
138
|
? 3600000
|
|
138
139
|
: cacheTTLNum;
|
|
139
140
|
|
|
141
|
+
const verifyOnStartRaw = env('ERRORS_LLM_VERIFY_ON_START') ?? '';
|
|
142
|
+
const verifyOnStart =
|
|
143
|
+
verifyOnStartRaw === true ||
|
|
144
|
+
verifyOnStartRaw === 'true' ||
|
|
145
|
+
verifyOnStartRaw === '1' ||
|
|
146
|
+
verifyOnStartRaw === 1;
|
|
147
|
+
|
|
140
148
|
return Object.freeze({
|
|
141
149
|
enabled: Boolean(enabled),
|
|
142
150
|
baseURL: String(baseURL ?? '').trim(),
|
|
@@ -150,11 +158,66 @@ export function getErrorsLlmConfig() {
|
|
|
150
158
|
rateLimit,
|
|
151
159
|
cache,
|
|
152
160
|
cacheTTL,
|
|
161
|
+
verifyOnStart,
|
|
153
162
|
});
|
|
154
163
|
}
|
|
155
164
|
|
|
156
165
|
export { MESSAGE_TYPES, LLM_MODES, LLM_CHANNELS };
|
|
157
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Fire a lightweight probe to the configured LLM provider and verify it
|
|
169
|
+
* responds correctly. Intended to run once at takeoff when `verifyOnStart: true`.
|
|
170
|
+
*
|
|
171
|
+
* Never throws — a flaky provider does not prevent the server from starting.
|
|
172
|
+
*
|
|
173
|
+
* @returns {Promise<{ ok: boolean, status: { feature: string, ok: boolean, detail: string } }>}
|
|
174
|
+
*/
|
|
175
|
+
export async function verifyLlmConnection() {
|
|
176
|
+
const { baseURL, apiKey, model, timeout, mode } = getErrorsLlmConfig();
|
|
177
|
+
|
|
178
|
+
const { createProvider } = await import('../lib/llm/index.js');
|
|
179
|
+
const provider = createProvider({ baseURL, apiKey, model, timeout });
|
|
180
|
+
|
|
181
|
+
const shortModel = model.split('/').pop().split(':')[0] || model;
|
|
182
|
+
const start = Date.now();
|
|
183
|
+
try {
|
|
184
|
+
const { content } = await provider.analyze(
|
|
185
|
+
'Respond with only the JSON object {"status":"ok"}. No explanation.',
|
|
186
|
+
);
|
|
187
|
+
const elapsed = Date.now() - start;
|
|
188
|
+
|
|
189
|
+
if (content.includes('"ok"')) {
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
status: {
|
|
193
|
+
feature: 'LLM Errors',
|
|
194
|
+
ok: true,
|
|
195
|
+
detail: `verified (${shortModel}, ${elapsed}ms, mode: ${mode})`,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
status: {
|
|
203
|
+
feature: 'LLM Errors',
|
|
204
|
+
ok: false,
|
|
205
|
+
detail: `unexpected response from ${shortModel} (${elapsed}ms)`,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
} catch (err) {
|
|
209
|
+
const elapsed = Date.now() - start;
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
status: {
|
|
213
|
+
feature: 'LLM Errors',
|
|
214
|
+
ok: false,
|
|
215
|
+
detail: `${err.message} (${elapsed}ms)`,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
158
221
|
/**
|
|
159
222
|
* Validate errors.llm when enabled: require baseURL, apiKey, and model (after LLM_ fallback).
|
|
160
223
|
* Also warns about misconfigurations (e.g. channel set with sync mode).
|
package/utils/startup.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read the framework's own package.json version.
|
|
7
|
+
* Resolves relative to the framework source, not the user's app.
|
|
8
|
+
* @returns {Promise<string>}
|
|
9
|
+
*/
|
|
10
|
+
export async function readFrameworkVersion() {
|
|
11
|
+
try {
|
|
12
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const raw = await readFile(path.join(dir, '..', 'package.json'), 'utf8');
|
|
14
|
+
return JSON.parse(raw).version ?? 'unknown';
|
|
15
|
+
} catch {
|
|
16
|
+
return 'unknown';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Format milliseconds into a compact human-readable string. */
|
|
21
|
+
export function fmtMs(ms) {
|
|
22
|
+
return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms)}ms`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SPINNER = [
|
|
26
|
+
'\u280B',
|
|
27
|
+
'\u2819',
|
|
28
|
+
'\u2839',
|
|
29
|
+
'\u2838',
|
|
30
|
+
'\u283C',
|
|
31
|
+
'\u2834',
|
|
32
|
+
'\u2826',
|
|
33
|
+
'\u2827',
|
|
34
|
+
'\u2807',
|
|
35
|
+
'\u280F',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a live status line writer for startup progress.
|
|
40
|
+
* On TTY terminals, shows a spinning animation that gets overwritten in-place
|
|
41
|
+
* when the step completes. On non-TTY (piped, CI), only prints the final result.
|
|
42
|
+
* @param {boolean} isTTY
|
|
43
|
+
*/
|
|
44
|
+
export function statusLine(isTTY) {
|
|
45
|
+
let timer = null;
|
|
46
|
+
|
|
47
|
+
function stop() {
|
|
48
|
+
if (timer) {
|
|
49
|
+
clearInterval(timer);
|
|
50
|
+
timer = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
start(feature, message) {
|
|
56
|
+
if (!isTTY) return;
|
|
57
|
+
let frame = 0;
|
|
58
|
+
const render = () => {
|
|
59
|
+
const s = SPINNER[frame % SPINNER.length];
|
|
60
|
+
process.stdout.write(
|
|
61
|
+
`\r\x1b[K ${feature.padEnd(14)} \x1b[36m${s}\x1b[0m \x1b[2m${message}\x1b[0m`,
|
|
62
|
+
);
|
|
63
|
+
frame++;
|
|
64
|
+
};
|
|
65
|
+
render();
|
|
66
|
+
timer = setInterval(render, 80);
|
|
67
|
+
},
|
|
68
|
+
finish(feature, ok, detail) {
|
|
69
|
+
stop();
|
|
70
|
+
if (isTTY) process.stdout.write('\r\x1b[K');
|
|
71
|
+
const icon =
|
|
72
|
+
ok === true
|
|
73
|
+
? '\x1b[32m\u2713\x1b[0m'
|
|
74
|
+
: ok === false
|
|
75
|
+
? '\x1b[31m\u2717\x1b[0m'
|
|
76
|
+
: '\x1b[2m\u2014\x1b[0m';
|
|
77
|
+
process.stdout.write(` ${feature.padEnd(14)} ${icon} ${detail}\n`);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
package/database/index.js
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import redis from './redis.js';
|
|
2
|
-
import mongodb from './mongodb.js';
|
|
3
|
-
import TejError from '../server/error.js';
|
|
4
|
-
import TejLogger from 'tej-logger';
|
|
5
|
-
|
|
6
|
-
const logger = new TejLogger('DatabaseManager');
|
|
7
|
-
|
|
8
|
-
class DatabaseManager {
|
|
9
|
-
static #instance = null;
|
|
10
|
-
static #isInitializing = false;
|
|
11
|
-
|
|
12
|
-
// Enhanced connection tracking with metadata
|
|
13
|
-
#connections = new Map();
|
|
14
|
-
#initializingConnections = new Map();
|
|
15
|
-
|
|
16
|
-
// Helper method for sleeping
|
|
17
|
-
async #sleep(ms) {
|
|
18
|
-
const { promise, resolve } = Promise.withResolvers();
|
|
19
|
-
setTimeout(resolve, ms);
|
|
20
|
-
return promise;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
constructor() {
|
|
24
|
-
if (DatabaseManager.#instance) {
|
|
25
|
-
return DatabaseManager.#instance;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (!DatabaseManager.#isInitializing) {
|
|
29
|
-
throw new TejError(
|
|
30
|
-
500,
|
|
31
|
-
'Use DatabaseManager.getInstance() to get the instance',
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
DatabaseManager.#isInitializing = false;
|
|
36
|
-
DatabaseManager.#instance = this;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
static getInstance() {
|
|
40
|
-
if (!DatabaseManager.#instance) {
|
|
41
|
-
DatabaseManager.#isInitializing = true;
|
|
42
|
-
DatabaseManager.#instance = new DatabaseManager();
|
|
43
|
-
}
|
|
44
|
-
return DatabaseManager.#instance;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async initializeConnection(dbType, config) {
|
|
48
|
-
const key = dbType.toLowerCase();
|
|
49
|
-
|
|
50
|
-
// If a connection already exists for this config, return it
|
|
51
|
-
if (this.#connections.has(key)) {
|
|
52
|
-
return this.#connections.get(key).client;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Set initializing flag
|
|
56
|
-
this.#initializingConnections.set(key, true);
|
|
57
|
-
|
|
58
|
-
let client;
|
|
59
|
-
try {
|
|
60
|
-
switch (key) {
|
|
61
|
-
case 'redis':
|
|
62
|
-
client = await redis.createConnection({
|
|
63
|
-
isCluster: config.isCluster || false,
|
|
64
|
-
options: config || {},
|
|
65
|
-
});
|
|
66
|
-
break;
|
|
67
|
-
case 'mongodb':
|
|
68
|
-
client = await mongodb.createConnection(config);
|
|
69
|
-
break;
|
|
70
|
-
default:
|
|
71
|
-
throw new TejError(400, `Unsupported database type: ${dbType}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
this.#connections.set(key, {
|
|
75
|
-
type: dbType,
|
|
76
|
-
client,
|
|
77
|
-
config,
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// Clear initializing flag
|
|
81
|
-
this.#initializingConnections.delete(key);
|
|
82
|
-
|
|
83
|
-
return client;
|
|
84
|
-
} catch (error) {
|
|
85
|
-
// Clear initializing flag on error
|
|
86
|
-
this.#initializingConnections.delete(key);
|
|
87
|
-
logger.error(`Failed to initialize ${dbType} connection:`, error);
|
|
88
|
-
throw error;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
getConnection(dbType) {
|
|
93
|
-
const key = dbType.toLowerCase();
|
|
94
|
-
const connection = this.#connections.get(key);
|
|
95
|
-
if (!connection) {
|
|
96
|
-
throw new TejError(
|
|
97
|
-
404,
|
|
98
|
-
`No connection found for ${dbType} with given config`,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
return connection.client;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async closeConnection(dbType, config) {
|
|
105
|
-
const key = dbType.toLowerCase();
|
|
106
|
-
if (!this.#connections.has(key)) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
const connection = this.#connections.get(key);
|
|
112
|
-
switch (key) {
|
|
113
|
-
case 'redis':
|
|
114
|
-
await redis.closeConnection(connection.client);
|
|
115
|
-
break;
|
|
116
|
-
case 'mongodb':
|
|
117
|
-
await mongodb.closeConnection(connection.client);
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
this.#connections.delete(key);
|
|
122
|
-
} catch (error) {
|
|
123
|
-
logger.error(`Error closing ${dbType} connection:`, error);
|
|
124
|
-
throw error;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Close all database connections
|
|
130
|
-
* @returns {Promise<void>}
|
|
131
|
-
*/
|
|
132
|
-
async closeAllConnections() {
|
|
133
|
-
const closePromises = [];
|
|
134
|
-
for (const [key, connection] of this.#connections) {
|
|
135
|
-
closePromises.push(
|
|
136
|
-
this.closeConnection(connection.type, connection.config),
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
await Promise.all(closePromises);
|
|
140
|
-
this.#connections.clear();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Get all active connections
|
|
145
|
-
* @returns {Map<string, {type: string, client: any, config: Object}>}
|
|
146
|
-
*/
|
|
147
|
-
getActiveConnections() {
|
|
148
|
-
return new Map(this.#connections);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Check if a connection exists or is being initialized
|
|
153
|
-
* @param {string} dbType - Type of database
|
|
154
|
-
* @param {Object} config - Database configuration
|
|
155
|
-
* @returns {{ exists: boolean, initializing: boolean }}
|
|
156
|
-
*/
|
|
157
|
-
hasConnection(dbType, config) {
|
|
158
|
-
const key = dbType.toLowerCase();
|
|
159
|
-
return {
|
|
160
|
-
exists: this.#connections.has(key),
|
|
161
|
-
initializing: this.#initializingConnections.has(key),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const dbManager = DatabaseManager.getInstance();
|
|
167
|
-
export default dbManager;
|