@emasoft/svg-matrix 1.0.6 → 1.0.8

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/src/logger.js ADDED
@@ -0,0 +1,302 @@
1
+ /**
2
+ * @fileoverview Configurable logging for @emasoft/svg-matrix
3
+ * Provides centralized logging control for all library modules.
4
+ *
5
+ * Why buffered writing: appendFileSync blocks the event loop on every log call.
6
+ * For high-volume logging, this creates significant performance impact.
7
+ * Instead, we buffer log messages and flush periodically or on demand.
8
+ * The tradeoff is that logs may be lost on crash - use flush() before
9
+ * critical operations if durability is needed.
10
+ *
11
+ * @module src/logger
12
+ * @license MIT
13
+ *
14
+ * @example
15
+ * import { Logger, setLogLevel, LogLevel } from '@emasoft/svg-matrix';
16
+ *
17
+ * // Suppress all logging
18
+ * setLogLevel(LogLevel.SILENT);
19
+ *
20
+ * // Enable only errors
21
+ * setLogLevel(LogLevel.ERROR);
22
+ *
23
+ * // Enable warnings and errors (default)
24
+ * setLogLevel(LogLevel.WARN);
25
+ *
26
+ * // Enable all logging including debug
27
+ * setLogLevel(LogLevel.DEBUG);
28
+ *
29
+ * // Or configure via Logger object
30
+ * Logger.level = LogLevel.WARN;
31
+ * Logger.logToFile = '/path/to/logfile.log';
32
+ */
33
+
34
+ // Why: Only appendFileSync is needed - we use sync writes in flush() for reliability
35
+ // during shutdown or before critical operations. Async writes were considered but
36
+ // the complexity of handling async flush during process exit wasn't worth it.
37
+ import { appendFileSync } from 'fs';
38
+
39
+ // ============================================================================
40
+ // CONSTANTS
41
+ // ============================================================================
42
+ // Why: Centralize configuration values to make tuning easier
43
+ const LOG_BUFFER_SIZE = 100; // Flush after this many messages
44
+ const LOG_FLUSH_INTERVAL_MS = 5000; // Auto-flush every 5 seconds
45
+
46
+ // ============================================================================
47
+ // LOG LEVELS
48
+ // ============================================================================
49
+ /**
50
+ * Log levels for controlling output verbosity.
51
+ * Why numeric values: Allows simple >= comparisons for level filtering.
52
+ * @enum {number}
53
+ */
54
+ export const LogLevel = {
55
+ /** Suppress all logging - use when library is used in production */
56
+ SILENT: 0,
57
+ /** Log only errors - problems that prevent operation */
58
+ ERROR: 1,
59
+ /** Log errors and warnings - issues that may indicate problems */
60
+ WARN: 2,
61
+ /** Log errors, warnings, and info - normal operation status */
62
+ INFO: 3,
63
+ /** Log everything including debug - for development/troubleshooting */
64
+ DEBUG: 4,
65
+ };
66
+
67
+ // ============================================================================
68
+ // LOGGER IMPLEMENTATION
69
+ // ============================================================================
70
+ /**
71
+ * Global logger configuration and methods.
72
+ * Why singleton pattern: Logging configuration should be consistent across
73
+ * all modules in the library. A single Logger object ensures this.
74
+ * @namespace
75
+ */
76
+ export const Logger = {
77
+ /**
78
+ * Current log level. Messages below this level are suppressed.
79
+ * Why WARN default: Library users typically only want to see problems,
80
+ * not routine operation info. They can increase to INFO/DEBUG if needed.
81
+ * @type {number}
82
+ */
83
+ level: LogLevel.WARN,
84
+
85
+ /**
86
+ * Optional file path for logging output.
87
+ * Why null default: File logging must be explicitly enabled to avoid
88
+ * unexpected file creation in user's project directories.
89
+ * @type {string|null}
90
+ */
91
+ logToFile: null,
92
+
93
+ /**
94
+ * Whether to include timestamps in log output.
95
+ * Why false default: Timestamps add noise for casual use. Enable for
96
+ * debugging timing issues or when correlating with other logs.
97
+ * @type {boolean}
98
+ */
99
+ timestamps: false,
100
+
101
+ /**
102
+ * Internal buffer for batching file writes.
103
+ * Why buffering: Reduces I/O overhead by batching multiple log messages
104
+ * into single file operations. See module docstring for tradeoffs.
105
+ * @type {string[]}
106
+ * @private
107
+ */
108
+ _buffer: [],
109
+
110
+ /**
111
+ * Timer reference for periodic flushing.
112
+ * Why: Ensures buffered messages are written even during idle periods.
113
+ * Without this, buffered messages might never be written if logging stops.
114
+ * @type {NodeJS.Timeout|null}
115
+ * @private
116
+ */
117
+ _flushTimer: null,
118
+
119
+ /**
120
+ * Format a log message with optional timestamp.
121
+ * Why centralized formatting: Ensures consistent log format across all
122
+ * log levels. Makes parsing and grep'ing logs easier.
123
+ * @param {string} level - Log level name
124
+ * @param {string} message - Message to format
125
+ * @returns {string} Formatted message
126
+ * @private
127
+ */
128
+ _format(level, message) {
129
+ if (this.timestamps) {
130
+ const ts = new Date().toISOString();
131
+ return `[${ts}] [${level}] ${message}`;
132
+ }
133
+ return `[${level}] ${message}`;
134
+ },
135
+
136
+ /**
137
+ * Add message to buffer and flush if needed.
138
+ * Why buffer + conditional flush: Balances write efficiency with
139
+ * message timeliness. Large bursts are batched, but messages aren't
140
+ * delayed indefinitely.
141
+ * @param {string} message - Message to buffer
142
+ * @private
143
+ */
144
+ _bufferWrite(message) {
145
+ if (!this.logToFile) return;
146
+
147
+ this._buffer.push(message);
148
+
149
+ // Why: Flush when buffer is full to prevent unbounded memory growth
150
+ if (this._buffer.length >= LOG_BUFFER_SIZE) {
151
+ this.flush();
152
+ }
153
+
154
+ // Why: Start auto-flush timer if not already running
155
+ if (!this._flushTimer) {
156
+ this._flushTimer = setInterval(() => this.flush(), LOG_FLUSH_INTERVAL_MS);
157
+ // Why: unref() prevents timer from keeping process alive when done
158
+ this._flushTimer.unref();
159
+ }
160
+ },
161
+
162
+ /**
163
+ * Flush buffered messages to file.
164
+ * Why public: Allows callers to force immediate write before critical
165
+ * operations or shutdown. Uses sync write during flush for reliability.
166
+ */
167
+ flush() {
168
+ if (!this.logToFile || this._buffer.length === 0) return;
169
+
170
+ try {
171
+ // Why sync here: flush() is called when reliability matters more
172
+ // than performance (shutdown, before risky operation)
173
+ const content = this._buffer.join('\n') + '\n';
174
+ appendFileSync(this.logToFile, content);
175
+ this._buffer = [];
176
+ } catch {
177
+ // Why silent: Can't log a logging failure. Clear buffer to prevent
178
+ // infinite growth if file is consistently unwritable.
179
+ this._buffer = [];
180
+ }
181
+ },
182
+
183
+ /**
184
+ * Log an error message. Always logged unless SILENT.
185
+ * Why always flush errors: Errors may precede crashes. Immediate
186
+ * write ensures the error is captured even if crash follows.
187
+ * @param {string} message - Error message
188
+ * @param {...any} args - Additional arguments
189
+ */
190
+ error(message, ...args) {
191
+ if (this.level >= LogLevel.ERROR) {
192
+ const formatted = this._format('ERROR', message);
193
+ console.error(formatted, ...args);
194
+ this._bufferWrite(formatted + (args.length ? ' ' + args.join(' ') : ''));
195
+ // Why: Errors are important enough to flush immediately
196
+ this.flush();
197
+ }
198
+ },
199
+
200
+ /**
201
+ * Log a warning message. Logged at WARN level and above.
202
+ * @param {string} message - Warning message
203
+ * @param {...any} args - Additional arguments
204
+ */
205
+ warn(message, ...args) {
206
+ if (this.level >= LogLevel.WARN) {
207
+ const formatted = this._format('WARN', message);
208
+ console.warn(formatted, ...args);
209
+ this._bufferWrite(formatted + (args.length ? ' ' + args.join(' ') : ''));
210
+ }
211
+ },
212
+
213
+ /**
214
+ * Log an info message. Logged at INFO level and above.
215
+ * @param {string} message - Info message
216
+ * @param {...any} args - Additional arguments
217
+ */
218
+ info(message, ...args) {
219
+ if (this.level >= LogLevel.INFO) {
220
+ const formatted = this._format('INFO', message);
221
+ console.log(formatted, ...args);
222
+ this._bufferWrite(formatted + (args.length ? ' ' + args.join(' ') : ''));
223
+ }
224
+ },
225
+
226
+ /**
227
+ * Log a debug message. Logged only at DEBUG level.
228
+ * @param {string} message - Debug message
229
+ * @param {...any} args - Additional arguments
230
+ */
231
+ debug(message, ...args) {
232
+ if (this.level >= LogLevel.DEBUG) {
233
+ const formatted = this._format('DEBUG', message);
234
+ console.log(formatted, ...args);
235
+ this._bufferWrite(formatted + (args.length ? ' ' + args.join(' ') : ''));
236
+ }
237
+ },
238
+
239
+ /**
240
+ * Clean up resources. Call before process exit.
241
+ * Why: Flushes any remaining buffered messages and clears the timer.
242
+ * Without this, messages in buffer would be lost on exit.
243
+ */
244
+ shutdown() {
245
+ if (this._flushTimer) {
246
+ clearInterval(this._flushTimer);
247
+ this._flushTimer = null;
248
+ }
249
+ this.flush();
250
+ },
251
+ };
252
+
253
+ /**
254
+ * Set the global log level.
255
+ * Why convenience function: Provides a more explicit API than directly
256
+ * modifying Logger.level. Also allows future validation or side effects.
257
+ * @param {number} level - Log level from LogLevel enum
258
+ */
259
+ export function setLogLevel(level) {
260
+ // Why: Validate level is within valid range to catch typos
261
+ if (level < LogLevel.SILENT || level > LogLevel.DEBUG) {
262
+ throw new Error(`Invalid log level: ${level}. Use LogLevel.SILENT (0) through LogLevel.DEBUG (4)`);
263
+ }
264
+ Logger.level = level;
265
+ }
266
+
267
+ /**
268
+ * Get the current log level.
269
+ * Why: Allows callers to save/restore log level around noisy operations.
270
+ * @returns {number} Current log level
271
+ */
272
+ export function getLogLevel() {
273
+ return Logger.level;
274
+ }
275
+
276
+ /**
277
+ * Enable file logging.
278
+ * Why separate function: Encapsulates the setup of file logging including
279
+ * timestamp configuration. Clearer intent than setting multiple properties.
280
+ * @param {string} filePath - Path to log file
281
+ * @param {boolean} [withTimestamps=true] - Include timestamps
282
+ */
283
+ export function enableFileLogging(filePath, withTimestamps = true) {
284
+ // Why: Don't accept empty/null paths - would cause confusing errors later
285
+ if (!filePath) {
286
+ throw new Error('File path required for enableFileLogging()');
287
+ }
288
+ Logger.logToFile = filePath;
289
+ Logger.timestamps = withTimestamps;
290
+ }
291
+
292
+ /**
293
+ * Disable file logging.
294
+ * Why: Clean shutdown of file logging. Flushes buffer to ensure no
295
+ * messages are lost, then clears the file path.
296
+ */
297
+ export function disableFileLogging() {
298
+ Logger.flush(); // Why: Don't lose buffered messages
299
+ Logger.logToFile = null;
300
+ }
301
+
302
+ export default Logger;