export interface Logger { log: (...args: unknown[]) => void; debug: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void; } type Supplier = () => T; type TraceSupplier = Supplier; export interface ITraceableLogger> extends Logger { addTracer: (traceSupplier: TraceSupplier) => L; } export type ITraceableTuple = [T, TraceSupplier]; export type ITraceableMapper, U> = ( t: ITraceable, ) => U; export interface ITraceable> { item: T; logger: L; map: (mapper: ITraceableMapper) => ITraceable; bimap: ( mapper: ITraceableMapper>, ) => ITraceable; peek: (peek: ITraceableMapper) => ITraceable; flatMap: ( mapper: ITraceableMapper>, ) => ITraceable; flatMapAsync( mapper: ITraceableMapper>>, ): ITraceable, L>; } export class TraceableLogger implements ITraceableLogger { private readonly logger: Logger = console; constructor( private readonly traces = [() => `[${new Date().toISOString()}]`], ) { } public debug(...args: unknown[]) { this.logger.debug("[DEBUG]", ...this.getPrefix(), args); } public log(...args: unknown[]) { this.logger.log("[INFO]", ...this.getPrefix(), args); } public warn(...args: unknown[]) { this.logger.warn("[WARN]", ...this.getPrefix(), args); } public error(...args: unknown[]) { this.logger.error("[ERROR]", ...this.getPrefix(), args); } public addTracer(traceSupplier: TraceSupplier) { return new TraceableLogger(this.traces.concat(traceSupplier)); } private getPrefix() { return this.traces.map((tracer) => tracer()); } } class TraceableImpl< T, L extends ITraceableLogger, > implements ITraceable { protected constructor(readonly item: T, readonly logger: L) {} public map(mapper: ITraceableMapper) { const result = mapper(this); return new TraceableImpl(result, this.logger); } public flatMap( mapper: ITraceableMapper>, ): ITraceable { return mapper(this); } public flatMapAsync( mapper: ITraceableMapper>>, ): ITraceable, L> { return new TraceableImpl( mapper(this).then(({ item }) => item), this.logger, ); } public peek(peek: ITraceableMapper) { peek(this); return this; } public bimap(mapper: ITraceableMapper>) { const [item, trace] = mapper(this); return new TraceableImpl(item, this.logger.addTracer(trace)); } static promiseify, U>( mapper: ITraceableMapper, ): ITraceableMapper, L, Promise> { return (traceablePromise) => traceablePromise.flatMapAsync(async (t) => { const item = await t.item; return t.map(() => item).map(mapper); }).item; } } export class Traceable extends TraceableImpl { static withClassTrace( c: C, ): ITraceableMapper> { return (t) => [t.item, () => c.constructor.name]; } static from(t: T) { return new Traceable(t, new TraceableLogger()); } }