@noxfly/noxus 2.0.0 → 2.1.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.
@@ -4,10 +4,28 @@
4
4
  * @author NoxFly
5
5
  */
6
6
 
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+
7
10
  /**
8
11
  * Logger is a utility class for logging messages to the console.
9
12
  */
10
- export type LogLevel = 'log' | 'info' | 'warn' | 'error' | 'debug' | 'comment';
13
+ export type LogLevel =
14
+ | 'debug'
15
+ | 'comment'
16
+ | 'log'
17
+ | 'info'
18
+ | 'warn'
19
+ | 'error'
20
+ | 'critical'
21
+ ;
22
+
23
+ interface FileLogState {
24
+ queue: string[];
25
+ isWriting: boolean;
26
+ }
27
+
28
+
11
29
 
12
30
  /**
13
31
  * Returns a formatted timestamp for logging.
@@ -25,15 +43,24 @@ function getPrettyTimestamp(): string {
25
43
  * @param color - The color to use for the log message.
26
44
  * @returns A formatted string that includes the timestamp, process ID, message type, and callee name.
27
45
  */
28
- function getLogPrefix(callee: string, messageType: string, color: string): string {
46
+ function getLogPrefix(callee: string, messageType: string, color?: string): string {
29
47
  const timestamp = getPrettyTimestamp();
30
48
 
31
- const spaces = ' '.repeat(10 - messageType.length);
49
+ const spaces = " ".repeat(10 - messageType.length);
50
+
51
+ let colReset = Logger.colors.initial;
52
+ let colCallee = Logger.colors.yellow;
53
+
54
+ if(color === undefined) {
55
+ color = "";
56
+ colReset = "";
57
+ colCallee = "";
58
+ }
32
59
 
33
- return `${color}[APP] ${process.pid} - ${Logger.colors.initial}`
60
+ return `${color}[APP] ${process.pid} - ${colReset}`
34
61
  + `${timestamp}${spaces}`
35
- + `${color}${messageType.toUpperCase()}${Logger.colors.initial} `
36
- + `${Logger.colors.yellow}[${callee}]${Logger.colors.initial}`;
62
+ + `${color}${messageType.toUpperCase()}${colReset} `
63
+ + `${colCallee}[${callee}]${colReset}`;
37
64
  }
38
65
 
39
66
  /**
@@ -43,13 +70,23 @@ function getLogPrefix(callee: string, messageType: string, color: string): strin
43
70
  * @param arg - The object to format.
44
71
  * @returns A formatted string representation of the object, with each line prefixed by the specified prefix.
45
72
  */
46
- function formatObject(prefix: string, arg: object): string {
73
+ function formatObject(prefix: string, arg: object, enableColor: boolean = true): string {
47
74
  const json = JSON.stringify(arg, null, 2);
48
75
 
76
+ let colStart = "";
77
+ let colLine = "";
78
+ let colReset = "";
79
+
80
+ if(enableColor) {
81
+ colStart = Logger.colors.darkGrey;
82
+ colLine = Logger.colors.grey;
83
+ colReset = Logger.colors.initial;
84
+ }
85
+
49
86
  const prefixedJson = json
50
87
  .split('\n')
51
- .map((line, idx) => idx === 0 ? `${Logger.colors.darkGrey}${line}` : `${prefix} ${Logger.colors.grey}${line}`)
52
- .join('\n') + Logger.colors.initial;
88
+ .map((line, idx) => idx === 0 ? `${colStart}${line}` : `${prefix} ${colLine}${line}`)
89
+ .join('\n') + colReset;
53
90
 
54
91
  return prefixedJson;
55
92
  }
@@ -63,14 +100,21 @@ function formatObject(prefix: string, arg: object): string {
63
100
  * @param color - The color to use for the formatted arguments.
64
101
  * @returns An array of formatted arguments, where strings are colored and objects are formatted with indentation.
65
102
  */
66
- function formattedArgs(prefix: string, args: any[], color: string): any[] {
103
+ function formattedArgs(prefix: string, args: any[], color?: string): any[] {
104
+ let colReset = Logger.colors.initial;
105
+
106
+ if(color === undefined) {
107
+ color = "";
108
+ colReset = "";
109
+ }
110
+
67
111
  return args.map(arg => {
68
- if(typeof arg === 'string') {
69
- return `${color}${arg}${Logger.colors.initial}`;
112
+ if(typeof arg === "string") {
113
+ return `${color}${arg}${colReset}`;
70
114
  }
71
115
 
72
- else if(typeof arg === 'object') {
73
- return formatObject(prefix, arg);
116
+ else if(typeof arg === "object") {
117
+ return formatObject(prefix, arg, color === "");
74
118
  }
75
119
 
76
120
  return arg;
@@ -88,8 +132,8 @@ function getCallee(): string {
88
132
  ?.trim()
89
133
  .match(/at (.+?)(?:\..+)? .+$/)
90
134
  ?.[1]
91
- ?.replace('Object', '')
92
- .replace(/^_/, '')
135
+ ?.replace("Object", "")
136
+ .replace(/^_/, "")
93
137
  || "App";
94
138
  return caller;
95
139
  }
@@ -101,20 +145,97 @@ function getCallee(): string {
101
145
  * @returns A boolean indicating whether the log level is enabled.
102
146
  */
103
147
  function canLog(level: LogLevel): boolean {
104
- return logLevelRank[level] >= logLevelRank[logLevel];
148
+ return logLevels.has(level);
105
149
  }
106
150
 
151
+ /**
152
+ * Writes a log message to a file asynchronously to avoid blocking the event loop.
153
+ * It batches messages if writing is already in progress.
154
+ * @param filepath - The path to the log file.
155
+ */
156
+ function processLogQueue(filepath: string): void {
157
+ const state = fileStates.get(filepath);
158
+
159
+ if(!state || state.isWriting || state.queue.length === 0) {
160
+ return;
161
+ }
162
+
163
+ state.isWriting = true;
164
+
165
+ // Optimization: Grab all pending messages to write in one go
166
+ const messagesToWrite = state.queue.join('\n') + '\n';
167
+ state.queue = []; // Clear the queue immediately
168
+
169
+ const dir = path.dirname(filepath);
170
+
171
+ // Using async IO to allow other operations
172
+ fs.mkdir(dir, { recursive: true }, (err) => {
173
+ if(err) {
174
+ console.error(`[Logger] Failed to create directory ${dir}`, err);
175
+ state.isWriting = false;
176
+ return;
177
+ }
178
+
179
+ fs.appendFile(filepath, messagesToWrite, { encoding: "utf-8" }, (err) => {
180
+ state.isWriting = false;
181
+
182
+ if(err) {
183
+ console.error(`[Logger] Failed to write log to ${filepath}`, err);
184
+ }
185
+
186
+ // If new messages arrived while we were writing, process them now
187
+ if(state.queue.length > 0) {
188
+ processLogQueue(filepath);
189
+ }
190
+ });
191
+ });
192
+ }
193
+
194
+ /**
195
+ * Adds a message to the file queue and triggers processing.
196
+ */
197
+ function enqueue(filepath: string, message: string): void {
198
+ if(!fileStates.has(filepath)) {
199
+ fileStates.set(filepath, { queue: [], isWriting: false });
200
+ }
201
+
202
+ const state = fileStates.get(filepath)!;
203
+ state.queue.push(message);
204
+
205
+ processLogQueue(filepath);
206
+ }
207
+
208
+ /**
209
+ *
210
+ */
211
+ function output(level: LogLevel, args: any[]): void {
212
+ if(!canLog(level)) {
213
+ return;
214
+ }
215
+
216
+ const callee = getCallee();
217
+
218
+ {
219
+ const prefix = getLogPrefix(callee, level, logLevelColors[level]);
220
+ const data = formattedArgs(prefix, args, logLevelColors[level]);
221
+
222
+ logLevelChannel[level](prefix, ...data);
223
+ }
224
+
225
+ {
226
+ const prefix = getLogPrefix(callee, level);
227
+ const data = formattedArgs(prefix, args);
228
+
229
+ const filepath = fileSettings.get(level)?.filepath;
230
+
231
+ if(filepath) {
232
+ const message = prefix + " " + data.join(" ");
233
+ enqueue(filepath, message);
234
+ }
235
+ }
236
+ }
107
237
 
108
- let logLevel: LogLevel = 'debug';
109
238
 
110
- const logLevelRank: Record<LogLevel, number> = {
111
- debug: 0,
112
- comment: 1,
113
- log: 2,
114
- info: 3,
115
- warn: 4,
116
- error: 5,
117
- };
118
239
 
119
240
  export namespace Logger {
120
241
 
@@ -122,10 +243,30 @@ export namespace Logger {
122
243
  * Sets the log level for the logger.
123
244
  * This function allows you to change the log level dynamically at runtime.
124
245
  * This won't affect the startup logs.
246
+ *
247
+ * If the parameter is a single LogLevel, all log levels with equal or higher severity will be enabled.
248
+
249
+ * If the parameter is an array of LogLevels, only the specified levels will be enabled.
250
+ *
125
251
  * @param level Sets the log level for the logger.
126
252
  */
127
- export function setLogLevel(level: LogLevel): void {
128
- logLevel = level;
253
+ export function setLogLevel(level: LogLevel | LogLevel[]): void {
254
+ logLevels.clear();
255
+
256
+ if(Array.isArray(level)) {
257
+ for(const lvl of level) {
258
+ logLevels.add(lvl);
259
+ }
260
+ }
261
+ else {
262
+ const targetRank = logLevelRank[level];
263
+
264
+ for(const [lvl, rank] of Object.entries(logLevelRank) as [LogLevel, number][]) {
265
+ if(rank >= targetRank) {
266
+ logLevels.add(lvl);
267
+ }
268
+ }
269
+ }
129
270
  }
130
271
 
131
272
  /**
@@ -135,12 +276,7 @@ export namespace Logger {
135
276
  * @param args The arguments to log.
136
277
  */
137
278
  export function log(...args: any[]): void {
138
- if(!canLog('log'))
139
- return;
140
-
141
- const callee = getCallee();
142
- const prefix = getLogPrefix(callee, "log", colors.green);
143
- console.log(prefix, ...formattedArgs(prefix, args, colors.green));
279
+ output("log", args);
144
280
  }
145
281
 
146
282
  /**
@@ -150,12 +286,7 @@ export namespace Logger {
150
286
  * @param args The arguments to log.
151
287
  */
152
288
  export function info(...args: any[]): void {
153
- if(!canLog('info'))
154
- return;
155
-
156
- const callee = getCallee();
157
- const prefix = getLogPrefix(callee, "info", colors.blue);
158
- console.info(prefix, ...formattedArgs(prefix, args, colors.blue));
289
+ output("info", args);
159
290
  }
160
291
 
161
292
  /**
@@ -165,12 +296,7 @@ export namespace Logger {
165
296
  * @param args The arguments to log.
166
297
  */
167
298
  export function warn(...args: any[]): void {
168
- if(!canLog('warn'))
169
- return;
170
-
171
- const callee = getCallee();
172
- const prefix = getLogPrefix(callee, "warn", colors.brown);
173
- console.warn(prefix, ...formattedArgs(prefix, args, colors.brown));
299
+ output("warn", args);
174
300
  }
175
301
 
176
302
  /**
@@ -180,21 +306,14 @@ export namespace Logger {
180
306
  * @param args The arguments to log.
181
307
  */
182
308
  export function error(...args: any[]): void {
183
- if(!canLog('error'))
184
- return;
185
-
186
- const callee = getCallee();
187
- const prefix = getLogPrefix(callee, "error", colors.red);
188
- console.error(prefix, ...formattedArgs(prefix, args, colors.red));
309
+ output("error", args);
189
310
  }
190
311
 
312
+ /**
313
+ * Logs a message to the console with log level ERROR and a grey color scheme.
314
+ */
191
315
  export function errorStack(...args: any[]): void {
192
- if(!canLog('error'))
193
- return;
194
-
195
- const callee = getCallee();
196
- const prefix = getLogPrefix(callee, "error", colors.grey);
197
- console.error(prefix, ...formattedArgs(prefix, args, colors.grey));
316
+ output("error", args);
198
317
  }
199
318
 
200
319
  /**
@@ -204,12 +323,7 @@ export namespace Logger {
204
323
  * @param args The arguments to log.
205
324
  */
206
325
  export function debug(...args: any[]): void {
207
- if(!canLog('debug'))
208
- return;
209
-
210
- const callee = getCallee();
211
- const prefix = getLogPrefix(callee, "debug", colors.purple);
212
- console.debug(prefix, ...formattedArgs(prefix, args, colors.purple));
326
+ output("debug", args);
213
327
  }
214
328
 
215
329
  /**
@@ -219,33 +333,98 @@ export namespace Logger {
219
333
  * @param args The arguments to log.
220
334
  */
221
335
  export function comment(...args: any[]): void {
222
- if(!canLog('comment'))
223
- return;
336
+ output("comment", args);
337
+ }
224
338
 
225
- const callee = getCallee();
226
- const prefix = getLogPrefix(callee, "comment", colors.grey);
227
- console.debug(prefix, ...formattedArgs(prefix, args, colors.grey));
339
+ /**
340
+ * Logs a message to the console with log level CRITICAL.
341
+ * This function formats the message with a timestamp, process ID, and the name of the caller function or class.
342
+ * It uses different colors for different log levels to enhance readability.
343
+ * @param args The arguments to log.
344
+ */
345
+ export function critical(...args: any[]): void {
346
+ output("critical", args);
347
+ }
348
+
349
+ /**
350
+ * Enables logging to a file output for the specified log levels.
351
+ * @param filepath The path to the log file.
352
+ * @param levels The log levels to enable file logging for. Defaults to all levels.
353
+ */
354
+ export function enableFileLogging(filepath: string, levels: LogLevel[] = ["debug", "comment", "log", "info", "warn", "error", "critical"]): void {
355
+ for(const level of levels) {
356
+ fileSettings.set(level, { filepath });
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Disables logging to a file output for the specified log levels.
362
+ * @param levels The log levels to disable file logging for. Defaults to all levels.
363
+ */
364
+ export function disableFileLogging(levels: LogLevel[] = ["debug", "comment", "log", "info", "warn", "error", "critical"]): void {
365
+ for(const level of levels) {
366
+ fileSettings.delete(level);
367
+ }
228
368
  }
229
369
 
230
370
 
231
371
  export const colors = {
232
- black: '\x1b[0;30m',
233
- grey: '\x1b[0;37m',
234
- red: '\x1b[0;31m',
235
- green: '\x1b[0;32m',
236
- brown: '\x1b[0;33m',
237
- blue: '\x1b[0;34m',
238
- purple: '\x1b[0;35m',
239
-
240
- darkGrey: '\x1b[1;30m',
241
- lightRed: '\x1b[1;31m',
242
- lightGreen: '\x1b[1;32m',
243
- yellow: '\x1b[1;33m',
244
- lightBlue: '\x1b[1;34m',
245
- magenta: '\x1b[1;35m',
246
- cyan: '\x1b[1;36m',
247
- white: '\x1b[1;37m',
248
-
249
- initial: '\x1b[0m'
372
+ black: "\x1b[0;30m",
373
+ grey: "\x1b[0;37m",
374
+ red: "\x1b[0;31m",
375
+ green: "\x1b[0;32m",
376
+ brown: "\x1b[0;33m",
377
+ blue: "\x1b[0;34m",
378
+ purple: "\x1b[0;35m",
379
+
380
+ darkGrey: "\x1b[1;30m",
381
+ lightRed: "\x1b[1;31m",
382
+ lightGreen: "\x1b[1;32m",
383
+ yellow: "\x1b[1;33m",
384
+ lightBlue: "\x1b[1;34m",
385
+ magenta: "\x1b[1;35m",
386
+ cyan: "\x1b[1;36m",
387
+ white: "\x1b[1;37m",
388
+
389
+ initial: "\x1b[0m"
250
390
  };
251
391
  }
392
+
393
+
394
+ const fileSettings: Map<LogLevel, { filepath: string }> = new Map();
395
+ const fileStates: Map<string, FileLogState> = new Map(); // filepath -> state
396
+
397
+ const logLevels: Set<LogLevel> = new Set();
398
+
399
+ const logLevelRank: Record<LogLevel, number> = {
400
+ debug: 0,
401
+ comment: 1,
402
+ log: 2,
403
+ info: 3,
404
+ warn: 4,
405
+ error: 5,
406
+ critical: 6,
407
+ };
408
+
409
+ const logLevelColors: Record<LogLevel, string> = {
410
+ debug: Logger.colors.purple,
411
+ comment: Logger.colors.grey,
412
+ log: Logger.colors.green,
413
+ info: Logger.colors.blue,
414
+ warn: Logger.colors.brown,
415
+ error: Logger.colors.red,
416
+ critical: Logger.colors.lightRed,
417
+ };
418
+
419
+ const logLevelChannel: Record<LogLevel, (message?: any, ...optionalParams: any[]) => void> = {
420
+ debug: console.debug,
421
+ comment: console.debug,
422
+ log: console.log,
423
+ info: console.info,
424
+ warn: console.warn,
425
+ error: console.error,
426
+ critical: console.error,
427
+ };
428
+
429
+
430
+ Logger.setLogLevel("debug");