/ app / utils / debug / debounced-logger.js
debounced-logger.js
  1  export const LOG_LEVELS = {
  2    ERROR: 0,
  3    WARN: 1,
  4    INFO: 2,
  5    DEBUG: 3,
  6    TRACE: 4,
  7  };
  8  
  9  const LOG_LEVEL_NAMES = {
 10    [LOG_LEVELS.ERROR]: 'ERROR',
 11    [LOG_LEVELS.WARN]: 'WARN',
 12    [LOG_LEVELS.INFO]: 'INFO',
 13    [LOG_LEVELS.DEBUG]: 'DEBUG',
 14    [LOG_LEVELS.TRACE]: 'TRACE',
 15  };
 16  
 17  const DEFAULT_CONFIG = {
 18    level: LOG_LEVELS.INFO,
 19    debounceTime: 5000,
 20    maxRepeats: 10,
 21    truncateObjects: true,
 22    maxObjectDepth: 2,
 23    maxArrayLength: 5,
 24    maxStringLength: 200,
 25    useGroups: true,
 26    showTimestamps: true,
 27  };
 28  
 29  let globalConfig = { ...DEFAULT_CONFIG };
 30  
 31  export function configureLogger(config) {
 32    globalConfig = { ...DEFAULT_CONFIG, ...config };
 33  }
 34  
 35  export function resetLoggerConfig() {
 36    globalConfig = { ...DEFAULT_CONFIG };
 37  }
 38  
 39  export class DebouncedLogger {
 40    constructor(prefix, config = {}) {
 41      this.prefix = prefix;
 42      this.config = { ...globalConfig, ...config };
 43      this.logCache = new Map();
 44      this.groupStack = [];
 45    }
 46  
 47    error(message, ...args) {
 48      this._log(LOG_LEVELS.ERROR, message, args);
 49    }
 50  
 51    warn(message, ...args) {
 52      this._log(LOG_LEVELS.WARN, message, args);
 53    }
 54  
 55    info(message, ...args) {
 56      this._log(LOG_LEVELS.INFO, message, args);
 57    }
 58  
 59    debug(message, ...args) {
 60      this._log(LOG_LEVELS.DEBUG, message, args);
 61    }
 62  
 63    trace(message, ...args) {
 64      this._log(LOG_LEVELS.TRACE, message, args);
 65    }
 66  
 67    group(label, collapsed = false) {
 68      if (!this.config.useGroups) {
 69        return;
 70      }
 71  
 72      const groupMethod = collapsed ? console.groupCollapsed : console.group;
 73      groupMethod(`[${this.prefix}] ${label}`);
 74      this.groupStack.push(label);
 75    }
 76  
 77    groupEnd() {
 78      if (!this.config.useGroups || this.groupStack.length === 0) {
 79        return;
 80      }
 81  
 82      console.groupEnd();
 83      this.groupStack.pop();
 84    }
 85  
 86    clearCache() {
 87      this.logCache.forEach(entry => {
 88        if (entry.timeoutId) {
 89          clearTimeout(entry.timeoutId);
 90        }
 91      });
 92  
 93      this.logCache.clear();
 94    }
 95  
 96    _log(level, message, args) {
 97      if (level > this.config.level) {
 98        return;
 99      }
100  
101      const processedArgs = this._processArgs(args);
102  
103      if (level <= LOG_LEVELS.WARN) {
104        this._writeLog(level, message, processedArgs);
105        return;
106      }
107  
108      const key = this._getCacheKey(level, message, processedArgs);
109  
110      if (this.logCache.has(key)) {
111        const entry = this.logCache.get(key);
112  
113        entry.count++;
114  
115        if (entry.timeoutId) {
116          clearTimeout(entry.timeoutId);
117        }
118  
119        if (entry.count >= this.config.maxRepeats) {
120          this._writeLog(level, message, processedArgs, entry.count);
121          this.logCache.delete(key);
122          return;
123        }
124  
125        entry.timeoutId = setTimeout(() => {
126          this._writeLog(level, message, processedArgs, entry.count);
127          this.logCache.delete(key);
128        }, this.config.debounceTime);
129  
130        this.logCache.set(key, entry);
131      } else {
132        this._writeLog(level, message, processedArgs);
133  
134        const timeoutId = setTimeout(() => {
135          this.logCache.delete(key);
136        }, this.config.debounceTime);
137  
138        this.logCache.set(key, { count: 1, timeoutId });
139      }
140    }
141  
142    _writeLog(level, message, args, repeatCount = 1) {
143      const logMethod = this._getLogMethod(level);
144      const prefix = this._getPrefix(level);
145  
146      let formattedMessage = `${prefix} ${message}`;
147  
148      if (repeatCount > 1) {
149        formattedMessage += ` (repeated ${repeatCount} times)`;
150      }
151  
152      logMethod(formattedMessage, ...args);
153    }
154  
155    _getLogMethod(level) {
156      switch (level) {
157        case LOG_LEVELS.ERROR:
158          return console.error;
159        case LOG_LEVELS.WARN:
160          return console.warn;
161        case LOG_LEVELS.INFO:
162          return console.info;
163        case LOG_LEVELS.DEBUG:
164          return console.debug;
165        case LOG_LEVELS.TRACE:
166          return console.debug;
167        default:
168          return console.log;
169      }
170    }
171  
172    _getPrefix(level) {
173      let prefix = `[${this.prefix}]`;
174  
175      if (this.config.showTimestamps) {
176        const now = new Date();
177        const timestamp = now.toISOString().split('T')[1].slice(0, 12);
178        prefix = `[${timestamp}] ${prefix}`;
179      }
180  
181      if (level <= LOG_LEVELS.WARN) {
182        prefix = `${prefix} [${LOG_LEVEL_NAMES[level]}]`;
183      }
184  
185      return prefix;
186    }
187  
188    _processArgs(args) {
189      if (!this.config.truncateObjects) {
190        return args;
191      }
192  
193      return args.map(arg => this._truncateValue(arg, this.config.maxObjectDepth));
194    }
195  
196    _truncateValue(value, depth) {
197      if (depth <= 0) {
198        return '[Object]';
199      }
200  
201      if (value === null || value === undefined) {
202        return value;
203      }
204  
205      if (typeof value === 'string') {
206        if (value.length > this.config.maxStringLength) {
207          return `${value.substring(0, this.config.maxStringLength)}...`;
208        }
209        return value;
210      }
211  
212      if (typeof value !== 'object') {
213        return value;
214      }
215  
216      if (Array.isArray(value)) {
217        if (value.length > this.config.maxArrayLength) {
218          return [
219            ...value
220              .slice(0, this.config.maxArrayLength)
221              .map(item => this._truncateValue(item, depth - 1)),
222            `... (${value.length - this.config.maxArrayLength} more items)`,
223          ];
224        }
225        return value.map(item => this._truncateValue(item, depth - 1));
226      }
227  
228      const result = {};
229      const keys = Object.keys(value);
230  
231      for (const key of keys.slice(0, this.config.maxArrayLength)) {
232        result[key] = this._truncateValue(value[key], depth - 1);
233      }
234  
235      if (keys.length > this.config.maxArrayLength) {
236        result['...'] = `(${keys.length - this.config.maxArrayLength} more properties)`;
237      }
238  
239      return result;
240    }
241  
242    _getCacheKey(level, message, args) {
243      try {
244        return `${level}:${message}:${JSON.stringify(args)}`;
245      } catch (e) {
246        return `${level}:${message}:${args.length}_args`;
247      }
248    }
249  }
250  
251  export function createLogger(prefix, config = {}) {
252    return new DebouncedLogger(prefix, config);
253  }