@ably/chat
Version:
Ably Chat is a set of purpose-built APIs for a host of chat features enabling you to create 1:1, 1:Many, Many:1 and Many:Many chat rooms for any scale. It is designed to meet a wide range of chat use cases, such as livestreams, in-game communication, cust
233 lines (197 loc) • 7.03 kB
text/typescript
import * as Ably from 'ably';
import { NormalizedChatClientOptions } from './config.js';
/**
* Interface for loggers.
*/
export interface Logger {
/**
* Log a message at the trace level.
* @param message The message to log.
* @param context The context of the log message as key-value pairs.
*/
trace(message: string, context?: LogContext): void;
/**
* Log a message at the debug level.
* @param message The message to log.
* @param context The context of the log message as key-value pairs.
*/
debug(message: string, context?: LogContext): void;
/**
* Log a message at the info level.
* @param message The message to log.
* @param context The context of the log message as key-value pairs.
*/
info(message: string, context?: LogContext): void;
/**
* Log a message at the warn level.
* @param message The message to log.
* @param context The context of the log message as key-value pairs.
*/
warn(message: string, context?: LogContext): void;
/**
* Log a message at the error level.
* @param message The message to log.
* @param context The context of the log message as key-value pairs.
*/
error(message: string, context?: LogContext): void;
/**
* Creates a new logger with a context that will be merged with any context provided to individual log calls.
* The context will be overridden by any matching keys in the individual log call's context.
* @param context The context to use for all log calls.
* @returns A new logger instance with the context.
*/
withContext(context: LogContext): Logger;
}
/**
* Represents the different levels of logging that can be used.
*/
export enum LogLevel {
/**
* Something routine and expected has occurred. This level will provide logs for the vast majority of operations
* and function calls.
*/
Trace = 'trace',
/**
* Development information, messages that are useful when trying to debug library behavior,
* but superfluous to normal operation.
*/
Debug = 'debug',
/**
* Informational messages. Operationally significant to the library but not out of the ordinary.
*/
Info = 'info',
/**
* Anything that is not immediately an error, but could cause unexpected behavior in the future. For example,
* passing an invalid value to an option. Indicates that some action should be taken to prevent future errors.
*/
Warn = 'warn',
/**
* A given operation has failed and cannot be automatically recovered. The error may threaten the continuity
* of operation.
*/
Error = 'error',
/**
* No logging will be performed.
*/
Silent = 'silent',
}
/**
* Represents the context of a log message.
* It is an object of key-value pairs that can be used to provide additional context to a log message.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type LogContext = Record<string, any>;
/**
* A function that can be used to handle log messages.
* @param message The message to log.
* @param level The log level of the message.
* @param context The context of the log message as key-value pairs.
*/
export type LogHandler = (message: string, level: LogLevel, context?: LogContext) => void;
/**
* A simple console logger that logs messages to the console.
* @param message The message to log.
* @param level The log level of the message.
* @param context - The context of the log message as key-value pairs.
*/
export const consoleLogger = (message: string, level: LogLevel, context?: LogContext) => {
const contextString = context ? `, context: ${JSON.stringify(context)}` : '';
const formattedMessage = `[${new Date().toISOString()}] ${level.valueOf().toUpperCase()} ably-chat: ${message}${contextString}`;
switch (level) {
case LogLevel.Trace:
case LogLevel.Debug: {
console.log(formattedMessage);
break;
}
case LogLevel.Info: {
console.info(formattedMessage);
break;
}
case LogLevel.Warn: {
console.warn(formattedMessage);
break;
}
case LogLevel.Error: {
console.error(formattedMessage);
break;
}
case LogLevel.Silent: {
break;
}
}
};
export const makeLogger = (options: NormalizedChatClientOptions): Logger => {
const logHandler = options.logHandler ?? consoleLogger;
return new DefaultLogger(logHandler, options.logLevel);
};
/**
* A convenient list of log levels as numbers that can be used for easier comparison.
*/
enum LogLevelNumber {
Trace = 0,
Debug = 1,
Info = 2,
Warn = 3,
Error = 4,
Silent = 5,
}
/**
* A mapping of log levels to their numeric equivalents.
*/
const logLevelNumberMap = new Map<LogLevel, LogLevelNumber>([
[LogLevel.Trace, LogLevelNumber.Trace],
[LogLevel.Debug, LogLevelNumber.Debug],
[LogLevel.Info, LogLevelNumber.Info],
[LogLevel.Warn, LogLevelNumber.Warn],
[LogLevel.Error, LogLevelNumber.Error],
[LogLevel.Silent, LogLevelNumber.Silent],
]);
/**
* A default logger implementation.
*/
class DefaultLogger implements Logger {
private readonly _handler: LogHandler;
private readonly _levelNumber: LogLevelNumber;
private readonly _context?: LogContext;
constructor(handler: LogHandler, level: LogLevel, context?: LogContext) {
this._handler = handler;
this._context = context;
const levelNumber = logLevelNumberMap.get(level);
if (levelNumber === undefined) {
throw new Ably.ErrorInfo(`Invalid log level: ${level}`, 50000, 500);
}
this._levelNumber = levelNumber;
}
trace(message: string, context?: LogContext): void {
this._write(message, LogLevel.Trace, LogLevelNumber.Trace, context);
}
debug(message: string, context?: LogContext): void {
this._write(message, LogLevel.Debug, LogLevelNumber.Debug, context);
}
info(message: string, context?: LogContext): void {
this._write(message, LogLevel.Info, LogLevelNumber.Info, context);
}
warn(message: string, context?: LogContext): void {
this._write(message, LogLevel.Warn, LogLevelNumber.Warn, context);
}
error(message: string, context?: LogContext): void {
this._write(message, LogLevel.Error, LogLevelNumber.Error, context);
}
withContext(context: LogContext): Logger {
// Get the original log level by finding the key in logLevelNumberMap that matches this._levelNumber
const originalLevel =
[...logLevelNumberMap.entries()].find(([, value]) => value === this._levelNumber)?.[0] ?? LogLevel.Error;
return new DefaultLogger(this._handler, originalLevel, this._mergeContext(context));
}
private _write(message: string, level: LogLevel, levelNumber: LogLevelNumber, context?: LogContext): void {
if (levelNumber >= this._levelNumber) {
this._handler(message, level, this._mergeContext(context));
}
}
private _mergeContext(context?: LogContext): LogContext | undefined {
if (!this._context) {
return context ?? undefined;
}
return context ? { ...this._context, ...context } : this._context;
}
}