UNPKG

syntropylog

Version:

An instance manager with observability for Node.js applications

4,273 lines 171 kB
import { EventEmitter } from 'events';
import { z, ZodError } from 'zod';
import RegexTest from 'regex-test';
import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'crypto';
import * as util from 'node:util';
import chalk from 'chalk';
import { createClient } from 'redis';

/**
 * @file src/logger/levels.ts
 * @description Defines the available log levels, their names, and their severity weights.
 */
/**
 * @description A mapping of log level names to their severity weights.
 * Higher numbers indicate higher severity.
 */
const LOG_LEVEL_WEIGHTS = {
    fatal: 60,
    error: 50,
    warn: 40,
    info: 30,
    debug: 20,
    trace: 10,
    silent: 0,
};

/**
 * @file src/logger/transports/Transport.ts
 * @description Defines the abstract base class for all log transports.
 */
/**
 * @class Transport
 * @description The abstract base class for all log transports. A transport is
 * responsible for the final output of a log entry, whether it's to the console,
 * a file, or a remote service.
 */
class Transport {
    /**
     * @constructor
     * @param {TransportOptions} [options] - The configuration options for this transport.
     */
    constructor(options = {}) {
        this.level = options.level ?? 'info';
        this.name = options.name ?? this.constructor.name;
        this.formatter = options?.formatter;
        this.sanitizationEngine = options?.sanitizationEngine;
    }
    /**
     * Determines if the transport should process a log entry based on its log level.
     * @param level - The level of the log entry to check.
     * @returns {boolean} - True if the transport is enabled for this level, false otherwise.
     */
    isLevelEnabled(level) {
        return LOG_LEVEL_WEIGHTS[level] >= LOG_LEVEL_WEIGHTS[this.level];
    }
    /**
     * A method to ensure all buffered logs are written before the application exits.
     * Subclasses should override this if they perform I/O buffering.
     * @returns {Promise<void>} A promise that resolves when flushing is complete.
     */
    async flush() {
        // Default implementation does nothing, assuming no buffering.
        return Promise.resolve();
    }
}

/**
 * FILE: src/masking/MaskingEngine.ts
 * DESCRIPTION: Ultra-fast data masking engine using JSON flattening strategy.
 *
 * This engine flattens complex nested objects into linear key-value pairs,
 * applies masking rules, and then reconstructs the original structure.
 * This approach provides extreme processing speed for any object depth.
 */
// Using type assertion for regex-test module since it lacks proper TypeScript declarations
/**
 * @enum MaskingStrategy
 * @description Different masking strategies for various data types.
 */
var MaskingStrategy;
(function (MaskingStrategy) {
    MaskingStrategy["CREDIT_CARD"] = "credit_card";
    MaskingStrategy["SSN"] = "ssn";
    MaskingStrategy["EMAIL"] = "email";
    MaskingStrategy["PHONE"] = "phone";
    MaskingStrategy["PASSWORD"] = "password";
    MaskingStrategy["TOKEN"] = "token";
    MaskingStrategy["CUSTOM"] = "custom";
})(MaskingStrategy || (MaskingStrategy = {}));
/**
 * @class MaskingEngine
 * Ultra-fast data masking engine using JSON flattening strategy.
 *
 * Instead of processing nested objects recursively, we flatten them to a linear
 * structure for extreme processing speed. This approach provides O(n) performance
 * regardless of object depth or complexity.
 */
class MaskingEngine {
    constructor(options) {
        /** @private Array of masking rules */
        this.rules = [];
        /** @private Whether the engine is initialized */
        this.initialized = false;
        this.maskChar = options?.maskChar || '*';
        this.preserveLength = options?.preserveLength ?? true; // Default to true for security
        this.regexTest = new RegexTest({ timeout: 100 });
        // Add default rules if enabled
        if (options?.enableDefaultRules !== false) {
            this.addDefaultRules();
        }
        // Add custom rules from options
        if (options?.rules) {
            for (const rule of options.rules) {
                this.addRule(rule);
            }
        }
    }
    /**
     * Adds default masking rules for common data types.
     * @private
     */
    addDefaultRules() {
        const defaultRules = [
            {
                pattern: /credit_card|card_number|payment_number/i,
                strategy: MaskingStrategy.CREDIT_CARD,
                preserveLength: true,
                maskChar: this.maskChar
            },
            {
                pattern: /ssn|social_security|security_number/i,
                strategy: MaskingStrategy.SSN,
                preserveLength: true,
                maskChar: this.maskChar
            },
            {
                pattern: /email/i,
                strategy: MaskingStrategy.EMAIL,
                preserveLength: true,
                maskChar: this.maskChar
            },
            {
                pattern: /phone|phone_number|mobile_number/i,
                strategy: MaskingStrategy.PHONE,
                preserveLength: true,
                maskChar: this.maskChar
            },
            {
                pattern: /password|pass|pwd|secret/i,
                strategy: MaskingStrategy.PASSWORD,
                preserveLength: true,
                maskChar: this.maskChar
            },
            {
                pattern: /token|api_key|auth_token|jwt|bearer/i,
                strategy: MaskingStrategy.TOKEN,
                preserveLength: true,
                maskChar: this.maskChar
            }
        ];
        for (const rule of defaultRules) {
            this.addRule(rule);
        }
    }
    /**
     * Adds a custom masking rule.
     * @param rule - The masking rule to add
     */
    addRule(rule) {
        // Compile regex pattern for performance
        if (typeof rule.pattern === 'string') {
            rule._compiledPattern = new RegExp(rule.pattern, 'i');
        }
        else {
            rule._compiledPattern = rule.pattern;
        }
        // Set defaults
        rule.preserveLength = rule.preserveLength ?? this.preserveLength;
        rule.maskChar = rule.maskChar ?? this.maskChar;
        this.rules.push(rule);
    }
    /**
     * Processes a metadata object and applies the configured masking rules.
     * Uses JSON flattening strategy for extreme performance.
     * @param meta - The metadata object to process
     * @returns A new object with the masked data
     */
    process(meta) {
        // Set initialized flag on first use
        if (!this.initialized) {
            this.initialized = true;
        }
        try {
            // Apply masking rules directly to the data structure
            const masked = this.applyMaskingRules(meta);
            // Return the masked data
            return masked;
        }
        catch (error) {
            // Silent observer - return original data if masking fails
            return meta;
        }
    }
    /**
     * Applies masking rules to data recursively.
     * @param data - Data to mask
     * @returns Masked data
     * @private
     */
    applyMaskingRules(data) {
        if (data === null || typeof data !== 'object') {
            return data;
        }
        if (Array.isArray(data)) {
            return data.map(item => this.applyMaskingRules(item));
        }
        const masked = { ...data };
        for (const key in data) {
            if (Object.prototype.hasOwnProperty.call(data, key)) {
                const value = data[key];
                if (typeof value === 'string') {
                    // Check each rule
                    for (const rule of this.rules) {
                        if (rule._compiledPattern && rule._compiledPattern.test(key)) {
                            masked[key] = this.applyStrategy(value, rule);
                            break; // First matching rule wins
                        }
                    }
                }
                else if (typeof value === 'object' && value !== null) {
                    // Recursively mask nested objects
                    masked[key] = this.applyMaskingRules(value);
                }
            }
        }
        return masked;
    }
    /**
     * Applies specific masking strategy to a value.
     * @param value - Value to mask
     * @param rule - Masking rule to apply
     * @returns Masked value
     * @private
     */
    applyStrategy(value, rule) {
        if (rule.strategy === MaskingStrategy.CUSTOM && rule.customMask) {
            return rule.customMask(value);
        }
        switch (rule.strategy) {
            case MaskingStrategy.CREDIT_CARD:
                return this.maskCreditCard(value, rule);
            case MaskingStrategy.SSN:
                return this.maskSSN(value, rule);
            case MaskingStrategy.EMAIL:
                return this.maskEmail(value, rule);
            case MaskingStrategy.PHONE:
                return this.maskPhone(value, rule);
            case MaskingStrategy.PASSWORD:
                return this.maskPassword(value, rule);
            case MaskingStrategy.TOKEN:
                return this.maskToken(value, rule);
            default:
                return this.maskDefault(value, rule);
        }
    }
    /**
     * Masks credit card number.
     * @param value - Credit card number
     * @param rule - Masking rule
     * @returns Masked credit card
     * @private
     */
    maskCreditCard(value, rule) {
        const clean = value.replace(/\D/g, '');
        if (rule.preserveLength) {
            // Preserve original format, mask all but last 4 digits
            return value.replace(/\d/g, (match, offset) => {
                const digitIndex = value.substring(0, offset).replace(/\D/g, '').length;
                return digitIndex < clean.length - 4 ? rule.maskChar : match;
            });
        }
        else {
            // Fixed format: ****-****-****-1111
            return `${rule.maskChar.repeat(4)}-${rule.maskChar.repeat(4)}-${rule.maskChar.repeat(4)}-${clean.slice(-4)}`;
        }
    }
    /**
     * Masks SSN.
     * @param value - SSN
     * @param rule - Masking rule
     * @returns Masked SSN
     * @private
     */
    maskSSN(value, rule) {
        const clean = value.replace(/\D/g, '');
        if (rule.preserveLength) {
            // Preserve original format, mask all but last 4 digits
            return value.replace(/\d/g, (match, offset) => {
                const digitIndex = value.substring(0, offset).replace(/\D/g, '').length;
                return digitIndex < clean.length - 4 ? rule.maskChar : match;
            });
        }
        else {
            // Fixed format: ***-**-6789
            return `***-**-${clean.slice(-4)}`;
        }
    }
    /**
     * Masks email address.
     * @param value - Email address
     * @param rule - Masking rule
     * @returns Masked email
     * @private
     */
    maskEmail(value, rule) {
        const atIndex = value.indexOf('@');
        if (atIndex > 0) {
            const username = value.substring(0, atIndex);
            const domain = value.substring(atIndex);
            if (rule.preserveLength) {
                // Preserve original length: first char + asterisks + @domain
                const maskedUsername = username.length > 1
                    ? username.charAt(0) + rule.maskChar.repeat(username.length - 1)
                    : rule.maskChar.repeat(username.length);
                return maskedUsername + domain;
            }
            else {
                return `${username.charAt(0)}***${domain}`;
            }
        }
        return this.maskDefault(value, rule);
    }
    /**
     * Masks phone number.
     * @param value - Phone number
     * @param rule - Masking rule
     * @returns Masked phone number
     * @private
     */
    maskPhone(value, rule) {
        const clean = value.replace(/\D/g, '');
        if (rule.preserveLength) {
            // Preserve original format, mask all but last 4 digits
            return value.replace(/\d/g, (match, offset) => {
                const digitIndex = value.substring(0, offset).replace(/\D/g, '').length;
                return digitIndex < clean.length - 4 ? rule.maskChar : match;
            });
        }
        else {
            // Fixed format: ***-***-4567
            return `${rule.maskChar.repeat(3)}-${rule.maskChar.repeat(3)}-${clean.slice(-4)}`;
        }
    }
    /**
     * Masks password.
     * @param value - Password
     * @param rule - Masking rule
     * @returns Masked password
     * @private
     */
    maskPassword(value, rule) {
        return rule.maskChar.repeat(value.length);
    }
    /**
     * Masks token.
     * @param value - Token
     * @param rule - Masking rule
     * @returns Masked token
     * @private
     */
    maskToken(value, rule) {
        if (rule.preserveLength) {
            return value.substring(0, 4) + rule.maskChar.repeat(value.length - 9) + value.substring(value.length - 5);
        }
        else {
            if (value.length > 8) {
                return value.substring(0, 4) + '...' + value.substring(value.length - 5);
            }
            return rule.maskChar.repeat(value.length);
        }
    }
    /**
     * Default masking strategy.
     * @param value - Value to mask
     * @param rule - Masking rule
     * @returns Masked value
     * @private
     */
    maskDefault(value, rule) {
        if (rule.preserveLength) {
            return rule.maskChar.repeat(value.length);
        }
        else {
            return rule.maskChar.repeat(Math.min(value.length, 8));
        }
    }
    /**
     * Gets masking engine statistics.
     * @returns Dictionary with masking statistics
     */
    getStats() {
        return {
            initialized: this.initialized,
            totalRules: this.rules.length,
            defaultRules: this.rules.filter(r => [MaskingStrategy.CREDIT_CARD, MaskingStrategy.SSN, MaskingStrategy.EMAIL,
                MaskingStrategy.PHONE, MaskingStrategy.PASSWORD, MaskingStrategy.TOKEN].includes(r.strategy)).length,
            customRules: this.rules.filter(r => r.strategy === MaskingStrategy.CUSTOM).length,
            strategies: this.rules.map(r => r.strategy)
        };
    }
    /**
     * Checks if the masking engine is initialized.
     * @returns True if initialized
     */
    isInitialized() {
        return this.initialized;
    }
    /**
     * Shutdown the masking engine.
     */
    shutdown() {
        this.rules = [];
        this.initialized = false;
    }
}

/**
 * FILE: src/config.schema.ts
 * DESCRIPTION: Defines the Zod validation schemas for the entire library's configuration.
 * These schemas are the single source of truth for the configuration's structure and types.
 */
/**
 * @description Schema for logger-specific options, including serialization and transports.
 * @private
 */
const loggerOptionsSchema = z
    .object({
    name: z.string().optional(),
    level: z
        .enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace', 'silent'])
        .optional(),
    serviceName: z.string().optional(),
    /**
     * An array of transport instances to be used by the logger.
     */
    transports: z.array(z.instanceof(Transport)).optional(),
    /**
     * A dictionary of custom serializer functions. The key is the field
     * to look for in the log object, and the value is the function that transforms it.
     */
    serializers: z
        .record(z.string(), z.function().args(z.any()).returns(z.string()))
        .optional(),
    /**
     * The maximum time in milliseconds a custom serializer can run before being timed out.
     * @default 50
     */
    serializerTimeoutMs: z.number().int().positive().default(50),
    /** Configuration for pretty printing logs in development. */
    prettyPrint: z
        .object({
        enabled: z.boolean().optional().default(false),
    })
        .optional(),
})
    .optional();
/**
 * @description Reusable schema for retry options, commonly used in client configurations.
 * @private
 */
const retryOptionsSchema = z
    .object({
    maxRetries: z.number().int().positive().optional(),
    retryDelay: z.number().int().positive().optional(),
})
    .optional();
/**
 * @description Schema for a single Redis instance, using a discriminated union for different connection modes.
 */
const redisInstanceConfigSchema = z.discriminatedUnion('mode', [
    z.object({
        mode: z.literal('single'),
        instanceName: z.string(),
        url: z.string().url(),
        retryOptions: retryOptionsSchema,
        // --- NEW: Granular Logging Configuration for Redis ---
        logging: z
            .object({
            /** Level for successful commands. @default 'debug' */
            onSuccess: z.enum(['trace', 'debug', 'info']).default('debug'),
            /** Level for failed commands. @default 'error' */
            onError: z.enum(['warn', 'error', 'fatal']).default('error'),
            /** Whether to log command parameters. @default true */
            logCommandValues: z.boolean().default(true),
            /** Whether to log the return value of commands. @default false */
            logReturnValue: z.boolean().default(false),
        })
            .optional(),
    }),
    // Apply the same 'logging' object structure to 'sentinel' and 'cluster' modes
    z.object({
        mode: z.literal('sentinel'),
        instanceName: z.string(),
        name: z.string(),
        sentinels: z.array(z.object({ host: z.string(), port: z.number() })),
        sentinelPassword: z.string().optional(),
        retryOptions: retryOptionsSchema,
        logging: z
            .object({
            onSuccess: z.enum(['trace', 'debug', 'info']).default('debug'),
            onError: z.enum(['warn', 'error', 'fatal']).default('error'),
            logCommandValues: z.boolean().default(true),
            logReturnValue: z.boolean().default(false),
        })
            .optional(),
    }),
    z.object({
        mode: z.literal('cluster'),
        instanceName: z.string(),
        rootNodes: z.array(z.object({ host: z.string(), port: z.number() })),
        logging: z
            .object({
            /** Level for successful commands. @default 'debug' */
            onSuccess: z.enum(['trace', 'debug', 'info']).default('debug'),
            /** Level for failed commands. @default 'error' */
            onError: z.enum(['warn', 'error', 'fatal']).default('error'),
            /** Whether to log command parameters. @default true */
            logCommandValues: z.boolean().default(true),
            /** Whether to log the return value of commands. @default false */
            logReturnValue: z.boolean().default(false),
        })
            .optional(),
    }),
]);
/**
 * @description Schema for the main Redis configuration block, containing all Redis instances.
 */
const redisConfigSchema = z
    .object({
    /** An array of Redis instance configurations. */
    instances: z.array(redisInstanceConfigSchema),
    /** The name of the default Redis instance to use when no name is provided to `getInstance()`. */
    default: z.string().optional(),
})
    .optional();
/**
 * @description Schema for a single HTTP client instance.
 */
const httpInstanceConfigSchema = z.object({
    instanceName: z.string(),
    adapter: z.custom((val) => {
        return (typeof val === 'object' &&
            val !== null &&
            'request' in val &&
            typeof val.request === 'function');
    }, "The provided adapter is invalid. It must be an object with a 'request' method."),
    isDefault: z.boolean().optional(),
    propagate: z.array(z.string()).optional(),
    propagateFullContext: z.boolean().optional(),
    logging: z
        .object({
        onSuccess: z.enum(['trace', 'debug', 'info']).default('info'),
        onError: z.enum(['warn', 'error', 'fatal']).default('error'),
        logSuccessBody: z.boolean().default(false),
        logSuccessHeaders: z.boolean().default(false),
        onRequest: z.enum(['trace', 'debug', 'info']).default('info'),
        logRequestBody: z.boolean().default(false),
        logRequestHeaders: z.boolean().default(false),
    })
        .partial()
        .optional(),
});
/**
 * @description Schema for the main HTTP configuration block.
 */
const httpConfigSchema = z
    .object({
    /** An array of HTTP client instance configurations. */
    instances: z.array(httpInstanceConfigSchema),
    /** The name of the default HTTP client instance to use when no name is provided to `getInstance()`. */
    default: z.string().optional(),
})
    .optional();
/**
 * @description Schema for the main data masking configuration block.
 */
const maskingConfigSchema = z
    .object({
    /** Array of masking rules with patterns and strategies. */
    rules: z.array(z.object({
        /** Regex pattern to match field names */
        pattern: z.union([z.string(), z.instanceof(RegExp)]),
        /** Masking strategy to apply */
        strategy: z.nativeEnum(MaskingStrategy),
        /** Whether to preserve original length */
        preserveLength: z.boolean().optional(),
        /** Character to use for masking */
        maskChar: z.string().optional(),
        /** Custom masking function (for CUSTOM strategy) */
        customMask: z.function().args(z.string()).returns(z.string()).optional(),
    })).optional(),
    /** Default mask character */
    maskChar: z.string().optional(),
    /** Whether to preserve original length by default */
    preserveLength: z.boolean().optional(),
    /** Enable default rules for common data types */
    enableDefaultRules: z.boolean().optional(),
})
    .optional();
/**
 * @description Schema for a single message broker client instance.
 * It validates that a valid `IBrokerAdapter` is provided.
 * @private
 */
const brokerInstanceConfigSchema = z.object({
    instanceName: z.string(),
    adapter: z.custom((val) => {
        return (typeof val === 'object' &&
            val !== null &&
            typeof val.publish === 'function' &&
            typeof val.subscribe === 'function');
    }, 'The provided broker adapter is invalid.'),
    /**
     * An array of context keys to propagate as message headers/properties.
     * To propagate all keys, provide an array with a single wildcard: `['*']`.
     * If not provided, only `correlationId` and `transactionId` are propagated by default.
     */
    propagate: z.array(z.string()).optional(),
    /**
     * @deprecated Use `propagate` instead.
     * If true, propagates the entire asynchronous context map as headers.
     * If false (default), only propagates `correlationId` and `transactionId`.
     */
    propagateFullContext: z.boolean().optional(),
    isDefault: z.boolean().optional(),
});
/**
 * @description Schema for the main message broker configuration block.
 */
const brokerConfigSchema = z
    .object({
    /** An array of broker client instance configurations. */
    instances: z.array(brokerInstanceConfigSchema),
    /** The name of the default broker instance to use when no name is provided to `getInstance()`. */
    default: z.string().optional(),
})
    .optional();
/**
 * @description Schema for the declarative logging matrix.
 * It controls which context properties are included in the final log output based on the log level.
 * @private
 */
const loggingMatrixSchema = z
    .object({
    /** An array of context keys to include in logs by default. Can be overridden by level-specific rules. */
    default: z.array(z.string()).optional(),
    /** An array of context keys to include for 'trace' level logs. Use `['*']` to include all context properties. */
    trace: z.array(z.string()).optional(),
    /** An array of context keys to include for 'debug' level logs. Use `['*']` to include all context properties. */
    debug: z.array(z.string()).optional(),
    /** An array of context keys to include for 'info' level logs. Use `['*']` to include all context properties. */
    info: z.array(z.string()).optional(),
    /** An array of context keys to include for 'warn' level logs. Use `['*']` to include all context properties. */
    warn: z.array(z.string()).optional(),
    /** An array of context keys to include for 'error' level logs. Use `['*']` to include all context properties. */
    error: z.array(z.string()).optional(),
    /** An array of context keys to include for 'fatal' level logs. Use `['*']` to include all context properties. */
    fatal: z.array(z.string()).optional(),
})
    .optional();
/**
 * @description The main schema for the entire SyntropyLog configuration.
 * This is the single source of truth for validating the user's configuration object.
 */
const syntropyLogConfigSchema = z.object({
    /** Logger-specific configuration. */
    logger: loggerOptionsSchema,
    /** Declarative matrix to control context data in logs. */
    loggingMatrix: loggingMatrixSchema,
    /** Redis client configuration. */
    redis: redisConfigSchema,
    /** HTTP client configuration. */
    http: httpConfigSchema,
    /** Message broker client configuration. */
    brokers: brokerConfigSchema,
    /** Centralized data masking configuration. */
    masking: maskingConfigSchema,
    /** Context propagation configuration. */
    context: z
        .object({
        /** The HTTP header name to use for the correlation ID. @default 'x-correlation-id' */
        correlationIdHeader: z.string().optional(),
        /** The HTTP header name to use for the external transaction/trace ID. @default 'x-trace-id' */
        transactionIdHeader: z.string().optional(),
    })
        .optional(),
    /**
     * The maximum time in milliseconds to wait for a graceful shutdown before timing out.
     * @default 5000
     */
    shutdownTimeout: z
        .number({
        description: 'The maximum time in ms to wait for a graceful shutdown.',
    })
        .int()
        .positive()
        .optional(),
});

// @file src/context/ContextManager.ts
// @description The default implementation of the IContextManager interface. It uses Node.js's
// `AsyncLocalStorage` to create and manage asynchronous contexts, enabling
// seamless propagation of data like correlation IDs across async operations.
/**
 * Manages asynchronous context using Node.js `AsyncLocalStorage`.
 * This is the core component for propagating context-specific data
 * (like correlation IDs) without passing them through function arguments.
 * @implements {IContextManager}
 */
class ContextManager {
    constructor(loggingMatrix) {
        this.storage = new AsyncLocalStorage();
        this.correlationIdHeader = 'x-correlation-id';
        this.transactionIdHeader = 'x-trace-id';
        this.storage = new AsyncLocalStorage();
        this.loggingMatrix = loggingMatrix;
    }
    configure(options) {
        if (options.correlationIdHeader) {
            this.correlationIdHeader = options.correlationIdHeader;
        }
        if (options.transactionIdHeader) {
            this.transactionIdHeader = options.transactionIdHeader;
        }
    }
    /**
     * Reconfigures the logging matrix dynamically.
     * This method allows changing which context fields are included in logs
     * without affecting security configurations like masking or log levels.
     * @param newMatrix The new logging matrix configuration
     */
    reconfigureLoggingMatrix(newMatrix) {
        this.loggingMatrix = newMatrix;
    }
    /**
     * Executes a function within a new, isolated asynchronous context.
     * Any data set via `set()` inside the callback will only be available
     * within that callback's asynchronous execution path. The new context
     * inherits values from the parent context, if one exists.
     * @template T The return type of the callback.
     * @param callback The function to execute within the new context.
     * @returns {T} The result of the callback function.
     */
    run(fn) {
        return new Promise((resolve, reject) => {
            const parentContext = this.storage.getStore();
            const newContextData = new Map(parentContext?.data);
            this.storage.run({ data: newContextData }, async () => {
                try {
                    await Promise.resolve(fn());
                    resolve();
                }
                catch (error) {
                    reject(error);
                }
            });
        });
    }
    /**
     * Gets a value from the current asynchronous context by its key.
     * @template T The expected type of the value.
     * @param key The key of the value to retrieve.
     * @returns The value, or `undefined` if not found or if outside a context.
     */
    get(key) {
        return this.storage.getStore()?.data.get(key);
    }
    /**
     * Gets the entire key-value store from the current asynchronous context.
     * @returns {ContextData} An object containing all context data, or an empty object if outside a context.
     */
    getAll() {
        const store = this.storage.getStore();
        if (!store) {
            return {};
        }
        return Object.fromEntries(store.data.entries());
    }
    /**
     * Sets a key-value pair in the current asynchronous context. This will have
     * no effect if called outside of a context created by `run()`.
     * This will only work if called within a context created by `run()`.
     * @param key The key for the value.
     * @param value The value to store.
     * @returns {void}
     */
    set(key, value) {
        const store = this.storage.getStore();
        if (store) {
            store.data.set(key, value);
        }
    }
    /**
     * Gets the correlation ID from the current context.
     * If no correlation ID exists, generates one automatically to ensure tracing continuity.
     * @returns {string} The correlation ID (never undefined).
     */
    getCorrelationId() {
        let correlationId = this.get(this.correlationIdHeader) || this.get('correlationId');
        if (!correlationId || typeof correlationId !== 'string') {
            // Generate correlationId if none exists to ensure tracing continuity
            correlationId = randomUUID();
            this.set(this.correlationIdHeader, correlationId);
        }
        return correlationId;
    }
    /**
     * Sets the correlation ID in the current context.
     * This sets the value in the configured header name.
     * @param correlationId The correlation ID to set.
     */
    setCorrelationId(correlationId) {
        this.set(this.correlationIdHeader, correlationId);
    }
    /**
     * Gets the transaction ID from the current context.
     * @returns {string | undefined} The transaction ID, or undefined if not set.
     */
    getTransactionId() {
        return this.get('transactionId');
    }
    /**
     * Sets the transaction ID in the current context.
     * @param transactionId The transaction ID to set.
     */
    setTransactionId(transactionId) {
        this.set('transactionId', transactionId);
    }
    /**
     * Gets the configured HTTP header name for the correlation ID.
     * @returns {string} The header name.
     */
    getCorrelationIdHeaderName() {
        return this.correlationIdHeader;
    }
    getTransactionIdHeaderName() {
        return this.transactionIdHeader;
    }
    /**
     * Gets the tracing headers to propagate the context (e.g., W3C Trace Context).
     * This base implementation does not support trace context propagation.
     * @returns `undefined` as this feature is not implemented by default.
     */
    getTraceContextHeaders() {
        const headers = {};
        // Only include headers if we're inside an active context
        const store = this.storage.getStore();
        if (!store) {
            return headers; // Return empty object if outside context
        }
        const correlationId = this.getCorrelationId();
        const transactionId = this.getTransactionId();
        if (correlationId) {
            headers[this.getCorrelationIdHeaderName()] = correlationId;
        }
        if (transactionId) {
            headers[this.getTransactionIdHeaderName()] = transactionId;
        }
        return headers;
    }
    getFilteredContext(level) {
        const fullContext = this.getAll();
        if (!this.loggingMatrix) {
            // Si no hay loggingMatrix, siempre incluir el correlationId
            const context = { ...fullContext };
            const headerCorrelationId = this.get(this.correlationIdHeader);
            const internalCorrelationId = this.get('correlationId');
            // Si no existe el correlationId del header, usar el interno
            if (!headerCorrelationId && internalCorrelationId) {
                context[this.correlationIdHeader] = internalCorrelationId;
            }
            return context;
        }
        const fieldsToKeep = this.loggingMatrix[level] ?? this.loggingMatrix.default;
        if (!fieldsToKeep) {
            return {};
        }
        // Mapeo de nombres de campos del loggingMatrix a claves reales del contexto
        const fieldMapping = {
            correlationId: [this.correlationIdHeader, 'correlationId'],
            transactionId: [this.transactionIdHeader, 'transactionId'],
            userId: ['userId'],
            serviceName: ['serviceName'],
            operation: ['operation'],
            errorCode: ['errorCode'],
            tenantId: ['tenantId'],
            paymentId: ['paymentId'],
            orderId: ['orderId'],
            processorId: ['processorId'],
            eventType: ['eventType'],
        };
        if (fieldsToKeep.includes('*')) {
            // Apply field mapping even for wildcard to ensure consistency
            const mappedContext = {};
            // Map all fields using the same logic as specific fields
            for (const [key, value] of Object.entries(fullContext)) {
                // Find the mapped field name for this key
                let mappedFieldName = key;
                for (const [matrixField, possibleKeys] of Object.entries(fieldMapping)) {
                    if (possibleKeys.includes(key)) {
                        mappedFieldName = matrixField;
                        break;
                    }
                }
                mappedContext[mappedFieldName] = value;
            }
            return mappedContext;
        }
        const filteredContext = {};
        for (const field of fieldsToKeep) {
            // Buscar en el mapeo de campos
            const possibleKeys = fieldMapping[field] || [field];
            // Buscar la primera clave que exista en el contexto
            for (const key of possibleKeys) {
                if (Object.prototype.hasOwnProperty.call(fullContext, key)) {
                    filteredContext[field] = fullContext[key];
                    break;
                }
            }
            // Si no se encontró en el mapeo, buscar directamente
            if (!Object.prototype.hasOwnProperty.call(filteredContext, field) &&
                Object.prototype.hasOwnProperty.call(fullContext, field)) {
                filteredContext[field] = fullContext[field];
            }
        }
        return filteredContext;
    }
}

/**
 * @file src/logger/Logger.ts
 * @description The core implementation of the ILogger interface.
 */
/**
 * @class Logger
 * @description The core logger implementation. It orchestrates the entire logging
 * pipeline, from argument parsing and level checking to serialization, masking,
 * and dispatching to transports.
 */
class Logger {
    constructor(name, transports, dependencies, options = {}) {
        this.name = name;
        this.transports = transports;
        this.dependencies = dependencies;
        this.bindings = options.bindings ?? {};
        this.level = options.level ?? 'info';
    }
    /**
     * @private
     * The core asynchronous logging method that runs the full processing pipeline.
     * It handles argument parsing, level filtering, serialization, masking,
     * and finally dispatches the processed log entry to the appropriate transports.
     * @param {LogLevel} level - The severity level of the log message.
     * @param {...(LogFormatArg | LogMetadata | JsonValue)[]} args - The arguments to be logged, following the Pino-like signature (e.g., `(obj, msg, ...)` or `(msg, ...)`).
     * @returns {Promise<void>}
     */
    async _log(level, ...args) {
        if (level === 'silent') {
            return;
        }
        // Type-guarded access to weights
        const weightedLevel = level;
        const weightedThisLevel = this.level;
        if (LOG_LEVEL_WEIGHTS[weightedLevel] < LOG_LEVEL_WEIGHTS[weightedThisLevel]) {
            return;
        }
        // Build the base log entry with context and bindings
        const context = this.dependencies.contextManager.getFilteredContext(level);
        const logEntry = {
            ...context,
            ...this.bindings,
            level,
            timestamp: new Date().toISOString(),
            service: this.name,
            message: '', // Will be set below
        };
        // Parse arguments following Pino-like signature
        let message;
        let metadata = {};
        if (args.length === 0) {
            message = '';
        }
        else if (typeof args[0] === 'object' &&
            args[0] !== null &&
            !Array.isArray(args[0])) {
            // First argument is metadata object: (metadata, message, ...formatArgs)
            metadata = args[0];
            message = args[1] || '';
            const formatArgs = args.slice(2);
            if (message && formatArgs.length > 0) {
                message = util.format(message, ...formatArgs);
            }
        }
        else {
            // First argument is message: (message, ...formatArgs)
            message = args[0] || '';
            const formatArgs = args.slice(1);
            if (message && formatArgs.length > 0) {
                message = util.format(message, ...formatArgs);
            }
        }
        // Ensure message is never undefined
        logEntry.message = message || '';
        // Merge metadata into log entry
        Object.assign(logEntry, metadata);
        // 1. Apply custom serializers (e.g., for Error objects)
        const finalEntry = await this.dependencies.serializerRegistry.process(logEntry, this);
        // 2. Apply masking to the entire, serialized entry.
        const maskedEntry = this.dependencies.maskingEngine.process(finalEntry);
        // Dispatch to transports
        await Promise.all(this.transports.map((transport) => {
            if (transport.isLevelEnabled(level)) {
                // The type assertion is safe here because the masking engine preserves the structure.
                return transport.log(maskedEntry);
            }
            return Promise.resolve();
        }));
    }
    /**
     * Logs a message at the 'info' level.
     * @param {...(LogFormatArg | LogMetadata | JsonValue)[]} args - The arguments to log.
     */
    info(...args) {
        return this._log('info', ...args);
    }
    /**
     * Logs a message at the 'warn' level.
     * @param {...(LogFormatArg | LogMetadata | JsonValue)[]} args - The arguments to log.
     */
    warn(...args) {
        return this._log('warn', ...args);
    }
    /**
     * Logs a message at the 'error' level.
     * @param {...(LogFormatArg | LogMetadata | JsonValue)[]} args - The arguments to log.
     */
    error(...args) {
        return this._log('error', ...args);
    }
    /**
     * Logs a message at the 'debug' level.
     * @param {...(LogFormatArg | LogMetadata | JsonValue)[]} args - The arguments to log.
     */
    debug(...args) {
        return this._log('debug', ...args);
    }
    /**
     * Logs a message at the 'trace' level.
     * @param {...(LogFormatArg | LogMetadata | JsonValue)[]} args - The arguments to log.
     */
    trace(...args) {
        return this._log('trace', ...args);
    }
    /**
     * Logs a message at the 'fatal' level.
     * @param {...(LogFormatArg | LogMetadata | JsonValue)[]} args - The arguments to log.
     */
    fatal(...args) {
        return this._log('fatal', ...args);
    }
    /**
     * Dynamically updates the minimum log level for this logger instance.
     * Any messages with a severity lower than the new level will be ignored.
     * @param {LogLevel} level - The new minimum log level.
     */
    setLevel(level) {
        this.level = level;
    }
    /**
     * Creates a new child logger instance that inherits the parent's configuration
     * and adds the specified bindings.
     * @param {LogBindings} bindings - Key-value pairs to bind to the child logger.
     * @returns {ILogger} A new logger instance with the specified bindings.
     */
    child(bindings) {
        const childLogger = new Logger(this.name, this.transports, this.dependencies, {
            level: this.level,
            bindings: { ...this.bindings, ...bindings },
        });
        return childLogger;
    }
    /**
     * Creates a new logger instance with a `source` field bound to it.
     * @param {string} source - The name of the source (e.g., 'redis', 'AuthModule').
     * @returns {ILogger} A new logger instance with the `source` binding.
     */
    withSource(source) {
        return this.child({ source });
    }
    /**
     * Creates a new logger instance with a `retention` field bound to it.
     * @param {LogRetentionRules} rules - A JSON object containing the retention rules.
     * @returns {ILogger} A new logger instance with the `retention` binding.
     */
    withRetention(rules) {
        return this.child({ retention: rules });
    }
    /**
     * Creates a new logger instance with a `transactionId` field bound to it.
     * @param {string} transactionId - The unique ID of the transaction.
     * @returns {ILogger} A new logger instance with the `transactionId` binding.
     */
    withTransactionId(transactionId) {
        return this.child({ transactionId });
    }
}

/**
 * @file src/serialization/SerializerRegistry.ts
 * @description Manages and safely applies custom log object serializers.
 */
/**
 * @class SerializerRegistry
 * @description Manages and applies custom serializer functions to log metadata.
 * It ensures that serializers are executed safely, with timeouts and error handling,
 * to prevent them from destabilizing the logging pipeline.
 */
class SerializerRegistry {
    /**
     * @constructor
     * @param {SerializerRegistryOptions} [options] - Configuration options for the registry.
     */
    constructor(options) {
        this.serializers = options?.serializers || {};
        this.timeoutMs = options?.timeoutMs || 50; // Default to a 50ms timeout
        // Add a default, built-in serializer for Error objects if one isn't provided.
        if (!this.serializers['err']) {
            this.serializers['err'] = this.defaultErrorSerializer;
        }
    }
    /**
     * Processes a metadata object, applying any matching serializers.
     * @param {Record<string, unknown>} meta - The metadata object from a log call.
     * @param {ILogger} logger - A logger instance to report errors from the serialization process itself.
     * @returns {Promise<Record<string, unknown>>} A new metadata object with serialized values.
     */
    async process(meta, logger) {
        const processedMeta = { ...meta };
        for (const key in processedMeta) {
            if (Object.prototype.hasOwnProperty.call(this.serializers, key)) {
                const serializerFn = this.serializers[key];
                const valueToSerialize = processedMeta[key];
                try {
                    // Execute the serializer within the secure executor
                    const serializedValue = await this.secureExecute(serializerFn, valueToSerialize);
                    processedMeta[key] = serializedValue;
                }
                catch (error) {
                    logger.warn(`Custom serializer for key "${key}" failed or timed out.`, { error: error instanceof Error ? error.message : String(error) });
                    processedMeta[key] =
                        `[SERIALIZER_ERROR: Failed to process key '${key}']`;
                }
            }
        }
        return processedMeta;
    }
    /**
     * @private
     * Safely executes a serializer function with a timeout.
     * @param {(value: unknown) => string} serializerFn - The serializer function to execute.
     * @param {unknown} value - The value to pass to the function.
     * @returns {Promise<string>} A promise that resolves with the serialized string.
     * @throws An error if the serializer throws an exception or times out.
     */
    secureExecute(serializerFn, value) {
        return new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                reject(new Error(`Serializer function timed out after ${this.timeoutMs}ms.`));
            }, this.timeoutMs);
            try {
                // We use Promise.resolve() to handle both sync and async serializers.
                Promise.resolve(serializerFn(value))
                    .then((result) => {
                    clearTimeout(timer);
                    resolve(result);
                })
                    .catch((err) => {
                    clearTimeout(timer);
                    reject(err);
                });
            }
            catch (err) {
                clearTimeout(timer);
                reject(err);
            }
        });
    }
    /**
     * @private
     * The default serializer for Error objects. It creates a JSON string representation
     * of the error, explicitly including common properties like name, message, and stack.
     * @param {unknown} err - The value to serialize, expected to be an Error.
     * @returns {string} A JSON string representing the error.
     */
    defaultErrorSerializer(err) {
        if (!(err instanceof Error)) {
            // For non-Error objects, a simple stringify is the best we can do.
            return JSON.stringify(err);
        }
        // For Error objects, explicitly pull out known, safe properties.
        const serializedError = {
            name: err.name,
            message: err.message,
            stack: err.stack,
        };
        // Include common additional properties if they exist.
        if ('cause' in err)
            serializedError.cause = err.cause;
        if ('code' in err)
            serializedError.code = err.code;
        return JSON.stringify(serializedError, null, 2);
    }
}

/**
 * @class ConsoleTransport
 * @description A transport that writes logs to the console as a single, serialized JSON string.
 * This format is ideal for log aggregation systems that can parse JSON.
 * @extends {Transport}
 */
class ConsoleTransport extends Transport {
    /**
     * @constructor
     * @param {TransportOptions} [options] - Options for the transport, including level, formatter, and a sanitization engine.
     */
    constructor(options) {
        super(options);
    }
    /**
     * Logs a structured entry to the console as a single JSON string.
     * The entry is first formatted (if a formatter is provided) and then sanitized
     * before being written to the console.
     * @param {LogEntry} entry - The log entry to process.
     * @returns {Promise<void>}
     */
    async log(entry) {
        if (!this.isLevelEnabled(entry.level)) {
            return;
        }
        const finalObject = this.formatter ? this.formatter.format(entry) : entry;
        const logString = JSON.stringify(finalObject);
        switch (entry.level) {
            case 'fatal':
            case 'error':
                console.error(logString);
                break;
            case 'warn':
                console.warn(logString);
                break;
            default:
                console.log(logString);
                break;
        }
    }
}

/**
 * @file src/sanitization/SanitizationEngine.ts
 * @description Final security layer that sanitizes log entries before they are written by a transport.
 */
/**
 * @class SanitizationEngine
 * A security engine that makes log entries safe for printing by stripping
 * potentially malicious control characters, such as ANSI escape codes.
 * This prevents log injection attacks that could exploit terminal vulnerabilities.
 */
class SanitizationEngine {
    /**
     * @constructor
     * The engine is currently not configurable, but the constructor is in place for future enhancements.
     */
    constructor(maskingEngine) {
        /** @private This regex matches ANSI escape codes used for colors, cursor movement, etc. */
        // prettier-ignore
        // eslint-disable-next-line no-control-regex
        this.ansiRegex = /[\x1b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
        this.maskingEngine = maskingEngine;
    }
    /**
     * Processes a log metadata object, sanitizing all its string values.
     * @param {Record<string, any>} meta - The metadata object to sanitize.
     * @returns {Record<string, any>} A new, sanitized metadata object.
     */
    process(meta) {
        let sanitized = this.sanitizeRecursively(meta);
        if (this.maskingEngine) {
            sanitized = this.maskingEngine.process(sanitized);
        }
        return sanitized;
    }
    /**
     * @private
     * Recursively traverses an object or array to sanitize all string values.
     * @param {any} data - The data to process.
     * @returns {any} The sanitized data.
     */
    sanitizeRecursively(data) {
        if (typeof data === 'string') {
            return data.replace(this.ansiRegex, '');
        }
        if (Array.isArray(data)) {
            return data.map((item) => this.sanitizeRecursively(item));
        }
        // Clave: Solo procesar objetos planos para no corromper instancias de clases.
        if (typeof data === 'object' &&
            data !== null &&
            data.constructor === Object) {
            const sanitizedObject = {};
            for (const key in data) {
                // hasOwnProperty sigue siendo una buena práctica aquí.
                if (Object.prototype.hasOwnProperty.call(data, key)) {
                    sanitizedObject[key] = this.sanitizeRecursively(data[key]);
                }
            }
            return sanitizedObject;
        }
        // Devuelve cualquier otro tipo de dato (números, booleans, instancias, etc.) sin modificar.
        return data;
    }
}

/**
 * @class LoggerFactory
 * @description Manages the lifecycle and configuration of all logging components.
 * An instance of this factory is created by `syntropyLog.init()` and acts as the central
 * orchestrator for creating and managing logger instances and their dependencies.
 */
class LoggerFactory {
    /**
     * @constructor
     * @param {SyntropyLogConfig} config - The global configuration object.
     * @param {IContextManager} contextManager - The shared context manager instance.
     * @param {SyntropyLog} syntropyLogInstance - The main framework instance for mediation.
     * @description Initializes all core logging engines and orchestrates transport setup.
     * It follows a key principle for transport configuration:
     * - **If `config.logger.transports` is provided:** The factory trusts the user's
     *   configuration completely and uses the provided transports as-is. It is the user's
     *   responsibility to configure them correctly (e.g., adding sanitization).
     * - **If no transports are provided:** The factory creates a single, production-safe
     *   `ConsoleTransport` by default, which includes a built-in `SanitizationEngine`.
     */
    constructor(config, contextManager, syntropyLogInstance) {
        /** @private A pool to cache logger instances by name for performance. */
        this.loggerPool = new Map();
        this.contextManager = contextManager;
        this.syntropyLogInstance = syntropyLogInstance;
        // Configure the context manager by passing the entire context config object.
        if (config.context) {
            this.contextManager.configure(config.context);
        }
        // Configure the HTTP manager if http instances are provided
        if (config.http?.instances) ;
        // If the user provides a specific list of transports, we use them directly.
        // We trust the user to have configured them correctly (e.g., providing a
        // SanitizationEngine to their production transports).
        if (config.logger?.transports) {
            this.transports = config.logger.transports;
        }
        else {
            // If no transports are provided, we create a safe, default production transport.
            // This transport includes a default sanitization engine.
            const sanitizationEngine = new SanitizationEngine();
            this.transports = [new ConsoleTransport({ sanitizationEngine })];
        }
        this.globalLogLevel = config.logger?.level ?? 'info';
        this.serviceName = config.logger?.serviceName ?? 'unknown-service';
        this.serializerRegistry = new SerializerRegistry({
            serializers: config.logger?.serializers,
            timeoutMs: config.logger?.serializerTimeoutMs,
        });
        this.maskingEngine = new MaskingEngine({
            rules: config.masking?.rules,
            maskChar: config.masking?.maskChar,
            preserveLength: config.masking?.preserveLength,
            enableDefaultRules: config.masking?.enableDefaultRules !== false,
        });
    }
    /**
     * Retrieves a logger instance by name. If the logger does not exist, it is created
     * and cached for subsequent calls.
     * @param {string} [name='default'] - The name of the logger to retrieve.
     * @param {Record<string, JsonValue>} [bindings] - Optional bindings to apply to the logger.
     * @returns {ILogger} The logger instance.
     */
    getLogger(name = 'default', bindings) {
        // Create a stable cache key that doesn't depend on object reference
        const cacheKey = this.createCacheKey(name, bindings);
        if (this.loggerPool.has(cacheKey)) {
            return this.loggerPool.get(cacheKey);
        }
        const loggerName = name === 'default' ? this.serviceName : name;
        const dependencies = {
            contextManager: this.contextManager,
            serializerRegistry: this.serializerRegistry,
            maskingEngine: this.maskingEngine,
            syntropyLogInstance: this.syntropyLogInstance,
        };
        const logger = new Logger(loggerName, this.transports, dependencies, {
            bindings,
        });
        logger.level = this.globalLogLevel;
        this.loggerPool.set(cacheKey, logger);
        return logger;
    }
    /**
     * Creates a stable cache key for logger instances.
     * @private
     */
    createCacheKey(name, bindings) {
        if (!bindings || Object.keys(bindings).length === 0) {
            return name;
        }
        // Sort keys to ensure consistent cache keys regardless of property order
        const sortedBindings = Object.keys(bindings)
            .sort()
            .reduce((result, key) => {
            result[key] = bindings[key];
            return result;
        }, {});
        try {
            return `${name}:${JSON.stringify(sortedBindings)}`;
        }
        catch {
            // Fallback for non-serializable objects
            return `${name}:${Object.keys(sortedBindings).sort().join(',')}`;
        }
    }
    /**
     * Calls the `flush` method on all configured transports to ensure buffered
     * logs are written before the application exits.
     */
    async flushAllTransports() {
        const flushPromises = this.transports.map((transport) => transport.flush().catch((err) => {
            console.error(`Error flushing transport ${transport.constructor.name}:`, err);
        }));
        await Promise.allSettled(flushPromises);
    }
    /**
     * Shuts down the logger factory and all its transports.
     * This ensures that all buffered logs are written and resources are cleaned up.
     */
    async shutdown() {
        try {
            // Flush all transports first
            await this.flushAllTransports();
            // Clear the logger pool
            this.loggerPool.clear();
            // Shutdown all transports if they have a shutdown method
            const shutdownPromises = this.transports.map((transport) => {
                if (typeof transport.shutdown ===
                    'function') {
                    return transport
                        .shutdown()
                        .catch((err) => {
                        console.error(`Error shutting down transport ${transport.constructor.name}:`, err);
                    });
                }
                return Promise.resolve();
            });
            await Promise.allSettled(shutdownPromises);
        }
        catch (error) {
            console.error('Error during LoggerFactory shutdown:', error);
        }
    }
}

/**
 * FILE: src/utils/sanitizeConfig.ts
 * DESCRIPTION: Utilities for sanitizing the SyntropyLog configuration object.
 */
const MASK = '[CONFIG_MASKED]';
const SENSITIVE_KEYS = [
    'password',
    'token',
    'secret',
    'apikey',
    'credential',
    'pass',
    'key',
    'accesstoken',
    'refreshtoken',
    'clientsecret',
    'sentinelpassword',
    'sasl',
];
/**
 * @private
 * A helper function to detect if a value is a special class instance
 * (like a Transport or an Adapter) that should not be deeply cloned or sanitized.
 * This is crucial to preserve methods and internal state of user-provided instances.
 * @param {any} value - The value to check.
 * @returns {value is Transport | IHttpClientAdapter | IBrokerAdapter} True if the value is a special instance.
 */
function isSpecialInstance(value) {
    if (value instanceof Transport) {
        return true;
    }
    // Duck-typing for adapters: if it has the core method, we treat it as an adapter.
    if (typeof value === 'object' &&
        value !== null &&
        (typeof value.request === 'function' ||
            typeof value.publish === 'function')) {
        return true;
    }
    return false;
}
/**
 * Recursively sanitizes a configuration object for safe logging.
 * It masks values for keys that are known to be sensitive and redacts credentials from URLs.
 * It intelligently skips special class instances (Transports, Adapters) to preserve their methods.
 * @param {T} config - The configuration object to sanitize.
 * @returns {T} A new, sanitized configuration object.
 */
function sanitizeConfig(config) {
    // If the object is a special instance (like a Transport or Adapter), return it without processing.
    if (isSpecialInstance(config)) {
        return config;
    }
    if (config === null || typeof config !== 'object') {
        return config;
    }
    if (Array.isArray(config)) {
        return config.map((item) => sanitizeConfig(item));
    }
    const sanitized = {};
    const sensitiveLower = SENSITIVE_KEYS.map((k) => k.toLowerCase());
    for (const key in config) {
        if (Object.prototype.hasOwnProperty.call(config, key)) {
            const lowerKey = key.toLowerCase();
            const value = config[key];
            if (sensitiveLower.includes(lowerKey)) {
                sanitized[key] = MASK;
            }
            else if ((lowerKey.includes('url') || lowerKey.includes('uri')) &&
                typeof value === 'string') {
                // Redact user:pass from connection strings.
                sanitized[key] = value.replace(/(?<=:\/\/)[^:]+:[^@]+@/, `${MASK}@`);
            }
            else if (typeof value === 'object' &&
                value !== null &&
                !(value instanceof RegExp)) {
                // The recursive call will also respect special instances.
                sanitized[key] = sanitizeConfig(value);
            }
            else {
                sanitized[key] = value;
            }
        }
    }
    return sanitized;
}

/**
 * @file src/http/InstrumentedHttpClient.ts
 * @description This class is the heart of the HTTP instrumentation architecture.
 * It wraps any adapter that complies with `IHttpClientAdapter` and adds a centralized
 * layer of instrumentation (logging, context, timers).
 */
/**
 * @class InstrumentedHttpClient
 * @description Wraps an `IHttpClientAdapter` to provide automatic logging,
 * context propagation, and timing for all HTTP requests.
 */
class InstrumentedHttpClient {
    /**
     * @constructor
     * @param {IHttpClientAdapter} adapter - The underlying HTTP client adapter (e.g., AxiosAdapter).
     * @param {ILogger} logger - The logger instance for this client.
     * @param {IContextManager} contextManager - The manager for handling asynchronous contexts.
     * @param {HttpClientInstanceConfig} config - The configuration for this specific instance.
     */
    constructor(adapter, logger, contextManager, config) {
        this.adapter = adapter;
        this.logger = logger;
        this.contextManager = contextManager;
        this.config = config;
        this.instanceName = config.instanceName;
        // Extract instrumentation options from the main config for clarity.
        this.instrumentorOptions = {
            logRequestHeaders: this.config.logging?.logRequestHeaders,
            logRequestBody: this.config.logging?.logRequestBody,
            logSuccessHeaders: this.config.logging?.logSuccessHeaders,
            logSuccessBody: this.config.logging?.logSuccessBody,
            logLevel: {
                onRequest: this.config.logging?.onRequest,
                onSuccess: this.config.logging?.onSuccess,
                onError: this.config.logging?.onError,
            },
        };
    }
    /**
     * The single public method. It executes an HTTP request through the wrapped
     * adapter, applying all instrumentation logic.
     * @template T The expected type of the response data.
     * @param {AdapterHttpRequest} request - The generic HTTP request to execute.
     * @returns {Promise<AdapterHttpResponse<T>>} A promise that resolves with the normalized response.
     * @throws {AdapterHttpError | Error} Throws the error from the adapter, which is re-thrown after being logged.
     */
    async request(request) {
        const startTime = Date.now();
        if (!request.headers) {
            request.headers = {};
        }
        // 1. Inject context into headers based on the configuration.
        if (this.config.propagate?.includes('*')) {
            // Wildcard behavior: Propagate the entire context map.
            const contextObject = this.contextManager.getAll();
            for (const key in contextObject) {
                if (Object.prototype.hasOwnProperty.call(contextObject, key)) {
                    const value = contextObject[key];
                    if (typeof value === 'string') {
                        request.headers[key] = value;
                    }
                }
            }
        }
        else if (this.config.propagate && Array.isArray(this.config.propagate)) {
            // New behavior: Propagate only specified context keys.
            for (const key of this.config.propagate) {
                const value = this.contextManager.get(key);
                if (typeof value === 'string') {
                    request.headers[key] = value;
                }
            }
        }
        else if (this.config.propagateFullContext) {
            // DEPRECATED: Propagate the entire context map.
            const contextObject = this.contextManager.getAll();
            for (const key in contextObject) {
                if (Object.prototype.hasOwnProperty.call(contextObject, key)) {
                    const value = contextObject[key];
                    if (typeof value === 'string') {
                        request.headers[key] = value;
                    }
                }
            }
        }
        // Always propagate correlation and transaction IDs, as they are fundamental.
        const correlationId = this.contextManager.getCorrelationId();
        if (correlationId) {
            request.headers[this.contextManager.getCorrelationIdHeaderName()] =
                correlationId;
        }
        const transactionId = this.contextManager.getTransactionId();
        if (transactionId) {
            request.headers[this.contextManager.getTransactionIdHeaderName()] =
                transactionId;
        }
        // 2. Log the start of the request.
        this.logRequestStart(request);
        try {
            // 3. Delegate execution to the adapter.
            const response = await this.adapter.request(request);
            const durationMs = Date.now() - startTime;
            // 4. Log the successful completion of the request.
            this.logRequestSuccess(request, response, durationMs);
            return response;
        }
        catch (error) {
            const durationMs = Date.now() - startTime;
            // 5. Log the failure of the request.
            this.logRequestFailure(request, error, durationMs);
            // 6. Re-throw the error so the user's code can handle it.
            throw error;
        }
    }
    /**
     * @private
     * Logs the start of an HTTP request, respecting the configured options.
     * @param {AdapterHttpRequest} request - The outgoing request.
     */
    logRequestStart(request) {
        const logLevel = this.instrumentorOptions.logLevel?.onRequest ?? 'info';
        const logPayload = {
            method: request.method,
            url: request.url,
        };
        if (this.instrumentorOptions.logRequestHeaders) {
            logPayload.headers = request.headers;
        }
        if (this.instrumentorOptions.logRequestBody) {
            logPayload.body = request.body;
        }
        this.logger[logLevel](logPayload, 'Starting HTTP request');
    }
    /**
     * @private
     * Logs the successful completion of an HTTP request.
     * @template T
     * @param {AdapterHttpRequest} request - The original request.
     * @param {AdapterHttpResponse<T>} response - The received response.
     * @param {number} durationMs - The total duration of the request in milliseconds.
     */
    logRequestSuccess(request, response, durationMs) {
        const logLevel = this.instrumentorOptions.logLevel?.onSuccess ?? 'info';
        const logPayload = {
            statusCode: response.statusCode,
            url: request.url,
            method: request.method,
            durationMs,
        };
        if (this.instrumentorOptions.logSuccessHeaders) {
            logPayload.headers = response.headers;
        }
        if (this.instrumentorOptions.logSuccessBody) {
            logPayload.body = response.data;
        }
        this.logger[logLevel](logPayload, 'HTTP response received');
    }
    /**
     * @private
     * Logs the failure of an HTTP request.
     * @param {AdapterHttpRequest} request - The original request.
     * @param {unknown} error - The error that was thrown.
     * @param {number} durationMs - The total duration of the request until failure.
     */
    logRequestFailure(request, error, durationMs) {
        const logLevel = this.instrumentorOptions.logLevel?.onError ?? 'error';
        // Use the normalized adapter error if available for richer logging.
        if (error && error.isAdapterError) {
            const adapterError = error;
            const logPayload = {
                err: adapterError, // The logger's serializer will handle this.
                url: request.url,
                method: request.method,
                durationMs,
                response: adapterError.response
                    ? {
                        statusCode: adapterError.response.statusCode,
                        headers: adapterError.response.headers,
                        body: adapterError.response.data,
                    }
                    : 'No response',
            };
            this.logger[logLevel](logPayload, 'HTTP request failed');
        }
        else {
            // If it's an unexpected error, log it as well.
            this.logger[logLevel]({ err: error, url: request.url, method: request.method, durationMs }, 'HTTP request failed with an unexpected error');
        }
    }
}

/**
 * Internal Types for SyntropyLog Framework
 *
 * These types and utilities are for advanced usage and internal framework operations.
 * Use with caution - they may change between versions.
 */
/**
 * Helper function to convert unknown error to JsonValue
 * Moved from @syntropylog/types to internal types
 */
function errorToJsonValue(error) {
    if (error instanceof Error) {
        return {
            name: error.name,
            message: error.message,
            stack: error.stack || null,
        };
    }
    return String(error);
}

/**
 * FILE: src/http/HttpManager.ts
 * @description Manages the lifecycle and creation of multiple instrumented HTTP client instances.
 */
/**
 * @class HttpManager
 * @description Manages the creation and retrieval of multiple instrumented HTTP client instances.
 * It reads the configuration, creates an `InstrumentedHttpClient` for each defined
 * instance by wrapping the user-provided adapter, and provides a way to retrieve them.
 */
class HttpManager {
    constructor(config, logger, contextManager) {
        /** @private A map storing the created instrumented client instances by name. */
        this.instances = new Map();
        this.config = config;
        this.logger = logger.child({ module: 'HttpManager' });
        this.contextManager = contextManager;
    }
    init() {
        this.logger.trace('Initializing HttpManager...');
        if (!this.config.instances || this.config.instances.length === 0) {
            this.logger.debug('HttpManager initialized, but no HTTP client instances were defined.');
            return;
        }
        for (const instanceConfig of this.config.instances) {
            try {
                const client = new InstrumentedHttpClient(instanceConfig.adapter, this.logger, this.contextManager, instanceConfig);
                this.instances.set(instanceConfig.instanceName, client);
                this.logger.info(`HTTP client instance "${instanceConfig.instanceName}" created successfully via adapter.`);
                if (instanceConfig.isDefault) {
                    if (this.defaultInstance) {
                        this.logger.warn(`Multiple default HTTP instances defined. Overwriting previous default "${this.defaultInstance.instanceName}" with "${instanceConfig.instanceName}".`);
                    }
                    this.logger.trace(`Setting default HTTP instance: ${instanceConfig.instanceName}`);
                    this.defaultInstance = client;
                }
            }
            catch (error) {
                this.logger.error({ error: errorToJsonValue(error) }, `Failed to create HTTP client instance "${instanceConfig.instanceName}"`);
            }
        }
        if (!this.defaultInstance && this.instances.size > 0) {
            const firstInstance = this.instances.values().next().value;
            const firstName = this.instances.keys().next().value;
            this.logger.trace(`No default HTTP instance configured. Using first available instance: ${firstName}`);
            this.defaultInstance = firstInstance;
        }
    }
    /**
     * Retrieves a managed and instrumented HTTP client instance by its name.
     * The returned client has a unified API via its `.request()` method.
     * @param {string} name - The name of the HTTP client instance to retrieve.
     * @returns {InstrumentedHttpClient} The requested client instance.
     * @throws {Error} If no instance with the given name is found.
     */
    getInstance(name) {
        const instanceName = name ?? this.defaultInstance?.instanceName;
        if (!instanceName) {
            throw new Error('A specific instance name was not provided and no default HTTP instance is configured.');
        }
        const instance = this.instances.get(instanceName);
        if (!instance) {
            throw new Error(`HTTP client instance with name "${instanceName}" was not found. Check your configuration.`);
        }
        return instance;
    }
    /**
     * Clears all managed HTTP client instances. This is a simple cleanup operation.
     */
    async shutdown() {
        this.logger.info('Shutting down HTTP clients.');
        this.instances.clear();
        // HTTP clients do not require explicit shutdown, so we just clear the map.
        return Promise.resolve();
    }
}

/**
 * @class InstrumentedBrokerClient
 * @description Wraps a user-provided broker adapter to automatically handle
 * logging, context propagation, and distributed tracing.
 */
class InstrumentedBrokerClient {
    /**
     * @constructor
     * @param {IBrokerAdapter} adapter - The concrete broker adapter implementation (e.g., for RabbitMQ, Kafka).
     * @param {ILogger} logger - The logger instance for this client.
     * @param {IContextManager} contextManager - The manager for handling asynchronous contexts.
     * @param {BrokerInstanceConfig} config - The configuration for this specific instance.
     */
    constructor(adapter, logger, contextManager, config) {
        this.adapter = adapter;
        this.logger = logger;
        this.contextManager = contextManager;
        this.config = config;
        this.instanceName = config.instanceName;
    }
    /**
     * Establishes a connection to the broker, wrapping the adapter's connect
     * method with logging.
     * @returns {Promise<void>}
     */
    async connect() {
        this.logger.info('Connecting to broker...');
        await this.adapter.connect();
        this.logger.info('Successfully connected to broker.');
    }
    /**
     * Disconnects from the broker, wrapping the adapter's disconnect method
     * with logging.
     * @returns {Promise<void>}
     */
    async disconnect() {
        this.logger.info('Disconnecting from broker...');
        await this.adapter.disconnect();
        this.logger.info('Successfully disconnected from broker.');
    }
    /**
     * Publishes a message, automatically injecting the current `correlation-id`
     * from the active context into the message headers.
     * @param {string} topic - The destination topic or routing key for the message.
     * @param {BrokerMessage} message - The message to be published. The `correlation-id`
     * will be added to its headers if not present.
     * @returns {Promise<void>}
     */
    async publish(topic, message) {
        if (!message.headers) {
            message.headers = {};
        }
        // Get current correlation ID from the active context (only if it exists, don't generate new)
        const currentCorrelationId = (this.contextManager.get(this.contextManager.getCorrelationIdHeaderName()) || this.contextManager.get('correlationId'));
        // 1. Inject context into headers based on the configuration.
        if (this.config.propagate?.includes('*')) {
            // Wildcard behavior: Propagate the entire context map.
            const contextObject = this.contextManager.getAll();
            for (const key in contextObject) {
                if (Object.prototype.hasOwnProperty.call(contextObject, key)) {
                    const value = contextObject[key];
                    if (typeof value === 'string' || Buffer.isBuffer(value)) {
                        message.headers[key] = value;
                    }
                }
            }
        }
        else if (this.config.propagate && Array.isArray(this.config.propagate)) {
            // New behavior: Propagate only specified context keys.
            for (const key of this.config.propagate) {
                const value = this.contextManager.get(key);
                if (typeof value === 'string' || Buffer.isBuffer(value)) {
                    message.headers[key] = value;
                }
            }
        }
        else if (this.config.propagateFullContext) {
            // DEPRECATED: Propagate the entire context map.
            const contextObject = this.contextManager.getAll();
            for (const key in contextObject) {
                if (Object.prototype.hasOwnProperty.call(contextObject, key)) {
                    const value = contextObject[key];
                    if (typeof value === 'string' || Buffer.isBuffer(value)) {
                        // Note: Broker headers typically support string | Buffer.
                        message.headers[key] = value;
                    }
                }
            }
        }
        // Only propagate correlation ID if it exists in the context (don't generate new)
        if (currentCorrelationId) {
            message.headers[this.contextManager.getCorrelationIdHeaderName()] =
                currentCorrelationId;
        }
        const transactionId = this.contextManager.getTransactionId();
        if (transactionId) {
            message.headers[this.contextManager.getTransactionIdHeaderName()] =
                transactionId;
        }
        this.logger.info({
            topic,
            messageId: message.headers?.['id'] instanceof Buffer
                ? message.headers?.['id'].toString()
                : message.headers?.['id'],
            correlationId: currentCorrelationId, // Log the correlation ID being used
        }, 'Publishing message...');
        await this.adapter.publish(topic, message);
        this.logger.info({
            topic,
            messageId: message.headers?.['id'] instanceof Buffer
                ? message.headers?.['id'].toString()
                : message.headers?.['id'],
            correlationId: currentCorrelationId, // Log the correlation ID being used
        }, 'Message published successfully.');
    }
    /**
     * Subscribes to a topic. It wraps the user's message handler to automatically
     * create a new asynchronous context for each incoming message. If a `correlation-id`
     * is found in the message headers, it is used to initialize the new context.
     * @param {string} topic - The topic or queue to subscribe to.
     * @param {MessageHandler} handler - The user-provided function to process incoming messages.
     * @returns {Promise<void>}
     */
    async subscribe(topic, handler) {
        this.logger.info({ topic }, 'Subscribing to topic...');
        // Wrap the user's handler to implement automatic context propagation.
        const instrumentedHandler = async (message, controls) => {
            // Get correlation ID from message headers first
            const messageCorrelationId = message.headers?.[this.contextManager.getCorrelationIdHeaderName()];
            // Get current correlation ID from context (but don't generate new one if not exists)
            const currentCorrelationId = this.contextManager.get(this.contextManager.getCorrelationIdHeaderName());
            // If message has different correlation ID, restore context from message
            if (messageCorrelationId &&
                messageCorrelationId !== currentCorrelationId) {
                await this.contextManager.run(async () => {
                    if (message.headers) {
                        for (const key in message.headers) {
                            this.contextManager.set(key, message.headers[key]);
                        }
                    }
                    // Use the message correlation ID for logging instead of generating a new one
                    const correlationId = messageCorrelationId;
                    this.logger.info({ topic, correlationId }, 'Received message.');
                    // Also wrap the lifecycle controls to add logging for ack/nack actions.
                    const instrumentedControls = {
                        ack: async () => {
                            await controls.ack();
                            this.logger.debug({ topic, correlationId }, 'Message acknowledged (ack).');
                        },
                        nack: async (requeue) => {
                            await controls.nack(requeue);
                            this.logger.warn({ topic, correlationId, requeue }, 'Message negatively acknowledged (nack).');
                        },
                    };
                    // Execute the original user-provided handler.
                    await handler(message, instrumentedControls);
                });
            }
            else {
                // Use current context, just set message headers if needed
                if (message.headers) {
                    for (const key in message.headers) {
                        this.contextManager.set(key, message.headers[key]);
                    }
                }
                // Use the message correlation ID if available, otherwise use current context (but don't generate new one)
                const correlationId = messageCorrelationId ||
                    this.contextManager.get(this.contextManager.getCorrelationIdHeaderName());
                this.logger.info({ topic, correlationId }, 'Received message.');
                // Also wrap the lifecycle controls to add logging for ack/nack actions.
                const instrumentedControls = {
                    ack: async () => {
                        await controls.ack();
                        this.logger.debug({ topic, correlationId }, 'Message acknowledged (ack).');
                    },
                    nack: async (requeue) => {
                        await controls.nack(requeue);
                        this.logger.warn({ topic, correlationId, requeue }, 'Message negatively acknowledged (nack).');
                    },
                };
                // Execute the original user-provided handler.
                await handler(message, instrumentedControls);
            }
        };
        await this.adapter.subscribe(topic, instrumentedHandler);
        this.logger.info({ topic }, 'Successfully subscribed to topic.');
    }
}

/**
 * FILE: src/brokers/BrokerManager.ts
 * DESCRIPTION:
 * Manages the lifecycle and creation of multiple instrumented broker client instances,
 * following the same pattern as HttpManager and RedisManager.
 */
/**
 * @class BrokerManager
 * @description Manages the lifecycle and creation of multiple instrumented broker client instances.
 * It reads the configuration, creates an `InstrumentedBrokerClient` for each defined
 * instance, and provides a way to retrieve them and shut them down gracefully.
 */
class BrokerManager {
    constructor(config, logger, contextManager) {
        this.instances = new Map();
        this.config = config;
        this.logger = logger.child({ module: 'BrokerManager' });
        this.contextManager = contextManager;
    }
    async init() {
        this.logger.trace('Initializing BrokerManager...');
        if (!this.config.instances || this.config.instances.length === 0) {
            this.logger.debug('BrokerManager initialized, but no broker instances were defined.');
            return;
        }
        const creationPromises = this.config.instances.map(async (instanceConfig) => {
            try {
                const client = new InstrumentedBrokerClient(instanceConfig.adapter, this.logger, this.contextManager, instanceConfig);
                await client.connect(); // Connect is likely async
                this.instances.set(instanceConfig.instanceName, client);
                this.logger.info(`Broker client instance "${instanceConfig.instanceName}" created and connected successfully.`);
                if (instanceConfig.instanceName === this.config.default) {
                    this.logger.trace(`Setting default broker instance: ${instanceConfig.instanceName}`);
                    this.defaultInstance = client;
                }
            }
            catch (error) {
                this.logger.error(`Failed to create broker instance "${instanceConfig.instanceName}":`, errorToJsonValue(error));
            }
        });
        await Promise.all(creationPromises);
        if (!this.defaultInstance && this.instances.size > 0) {
            const firstInstance = this.instances.values().next().value;
            const firstName = this.instances.keys().next().value;
            this.logger.trace(`No default broker instance configured. Using first available instance: ${firstName}`);
            this.defaultInstance = firstInstance;
        }
    }
    getInstance(name) {
        const instanceName = name ?? this.defaultInstance?.instanceName;
        if (!instanceName) {
            throw new Error('A specific instance name was not provided and no default Broker instance is configured.');
        }
        const instance = this.instances.get(instanceName);
        if (!instance) {
            throw new Error(`Broker client instance with name "${instanceName}" was not found. Check your configuration.`);
        }
        return instance;
    }
    async shutdown() {
        this.logger.info('Disconnecting all broker clients...');
        const shutdownPromises = Array.from(this.instances.values()).map((instance) => instance.disconnect());
        await Promise.allSettled(shutdownPromises);
    }
}

class LifecycleManager extends EventEmitter {
    constructor(syntropyFacade) {
        super();
        this.state = 'NOT_INITIALIZED';
        this.logger = null;
        this.syntropyFacade = syntropyFacade;
        // Initialize properties here to satisfy TypeScript's strict checks
        this.config = {};
        this.serializerRegistry = new SerializerRegistry({});
        this.maskingEngine = new MaskingEngine({});
    }
    getState() {
        return this.state;
    }
    async init(config) {
        if (this.state !== 'NOT_INITIALIZED') {
            this.logger?.warn(`LifecycleManager.init() called while in state '${this.state}'. Ignoring subsequent call.`);
            return;
        }
        this.state = 'INITIALIZING';
        try {
            const parsedConfig = syntropyLogConfigSchema.parse(config);
            const sanitizedConfig = sanitizeConfig(parsedConfig);
            this.config = sanitizedConfig;
            this.contextManager = new ContextManager(this.config.loggingMatrix);
            if (this.config.context) {
                this.contextManager.configure(this.config.context);
            }
            this.serializerRegistry = new SerializerRegistry({
                serializers: this.config.logger?.serializers,
                timeoutMs: this.config.logger?.serializerTimeoutMs,
            });
            this.maskingEngine = new MaskingEngine({
                rules: this.config.masking?.rules,
                maskChar: this.config.masking?.maskChar,
                preserveLength: this.config.masking?.preserveLength,
                enableDefaultRules: this.config.masking?.enableDefaultRules !== false,
            });
            this.loggerFactory = new LoggerFactory(this.config, this.contextManager, this.syntropyFacade);
            const logger = this.loggerFactory.getLogger('syntropylog-main');
            this.logger = logger;
            if (this.config.redis) {
                try {
                    const { RedisManager } = await Promise.resolve().then(function () { return RedisManager$1; });
                    this.redisManager = new RedisManager(this.config.redis, logger.withSource('redis-manager'), this.contextManager);
                    this.redisManager.init();
                }
                catch (error) {
                    logger.error('Failed to initialize Redis manager. Make sure redis package is installed.', { error: errorToJsonValue(error) });
                }
            }
            if (this.config.http) {
                this.httpManager = new HttpManager(this.config.http, logger.withSource('http-manager'), this.contextManager);
                this.httpManager.init();
            }
            if (this.config.brokers) {
                this.brokerManager = new BrokerManager(this.config.brokers, logger.withSource('broker-manager'), this.contextManager);
                await this.brokerManager.init();
            }
            logger.info('SyntropyLog framework initialized successfully.');
            this.state = 'READY';
            this.emit('ready');
        }
        catch (error) {
            this.state = 'ERROR';
            this.emit('error', error);
            if (error instanceof ZodError) {
                console.error('[SyntropyLog] Configuration validation failed:', error.errors);
            }
            else {
                console.error('[SyntropyLog] Failed to initialize framework:', error);
            }
            throw error;
        }
    }
    async shutdown() {
        this.logger?.info(`🔄 LifecycleManager.shutdown() called. Current state: ${this.state}`);
        if (this.state !== 'READY') {
            this.logger?.warn(`❌ Cannot perform shutdown. Current state: ${this.state}`);
            return;
        }
        this.state = 'SHUTTING_DOWN';
        this.emit('shutting_down');
        this.logger?.info('🔄 State changed to SHUTTING_DOWN');
        try {
            this.logger?.info('Shutting down SyntropyLog framework...');
            const shutdownPromises = [
                this.redisManager?.shutdown(),
                this.brokerManager?.shutdown(),
                this.httpManager?.shutdown(),
                this.loggerFactory?.shutdown?.(),
            ].filter(Boolean);
            this.logger?.info(`📋 Executing ${shutdownPromises.length} shutdown promises...`);
            await Promise.allSettled(shutdownPromises);
            this.logger?.info('✅ Shutdown promises completed');
            // Terminate external processes that might keep the process active
            this.logger?.info('🔍 Starting external process termination...');
            await this.terminateExternalProcesses();
            this.logger?.info('All managers have been shut down.');
            this.state = 'SHUTDOWN';
            this.emit('shutdown');
            this.logger?.info('✅ State changed to SHUTDOWN');
        }
        catch (error) {
            this.state = 'ERROR';
            this.emit('error', error);
            this.logger?.error('❌ Error during shutdown:', {
                error: errorToJsonValue(error),
            });
        }
    }
    /**
     * Terminates external processes that might keep the Node.js process active.
     * This includes regex-test workers and other child processes.
     */
    async terminateExternalProcesses() {
        try {
            this.logger?.info('🔍 Starting external process termination...');
            // Get all active handles
            const activeHandles = process._getActiveHandles?.() || [];
            this.logger?.debug(`Total active handles: ${activeHandles.length}`);
            // Filter child processes that need to be terminated
            const childProcesses = activeHandles.filter((handle) => {
                const isChildProcess = handle.constructor.name === 'ChildProcess';
                const isConnected = handle.connected;
                const hasRegexTest = handle.spawnargs?.some((arg) => arg.includes('regex-test'));
                this.logger?.debug(`Handle: ${handle.constructor.name}, connected: ${isConnected}, hasRegexTest: ${hasRegexTest}`);
                return isChildProcess && isConnected && hasRegexTest;
            });
            this.logger?.info(`Found ${childProcesses.length} regex-test processes to terminate`);
            if (childProcesses.length > 0) {
                this.logger?.info(`Terminating ${childProcesses.length} external processes...`);
                // Terminate each child process directly with SIGKILL for maximum effectiveness
                for (const childProcess of childProcesses) {
                    try {
                        this.logger?.debug(`Terminating process ${childProcess.pid} with SIGKILL...`);
                        childProcess.kill('SIGKILL');
                        this.logger?.debug(`Process ${childProcess.pid} terminated with SIGKILL`);
                    }
                    catch (error) {
                        this.logger?.warn(`Error terminating process ${childProcess.pid}:`, { error: errorToJsonValue(error) });
                    }
                }
                // Wait a bit for processes to terminate
                this.logger?.debug('Waiting 200ms for processes to terminate...');
                await new Promise((resolve) => setTimeout(resolve, 200));
                // Check if processes are still active
                const remainingHandles = process._getActiveHandles?.() || [];
                const remainingChildProcesses = remainingHandles.filter((handle) => handle.constructor.name === 'ChildProcess' &&
                    handle.connected &&
                    handle.spawnargs?.some((arg) => arg.includes('regex-test')));
                if (remainingChildProcesses.length > 0) {
                    this.logger?.warn(`${remainingChildProcesses.length} regex-test processes still active after SIGKILL`);
                    // Try to disconnect the processes
                    for (const childProcess of remainingChildProcesses) {
                        try {
                            childProcess.disconnect();
                            this.logger?.debug(`Process ${childProcess.pid} disconnected`);
                        }
                        catch (error) {
                            this.logger?.warn(`Error disconnecting process ${childProcess.pid}:`, { error: errorToJsonValue(error) });
                        }
                    }
                }
                else {
                    this.logger?.info('✅ All regex-test processes terminated successfully');
                }
            }
            else {
                this.logger?.info('No regex-test processes found to terminate');
            }
        }
        catch (error) {
            this.logger?.warn('Error terminating external processes:', {
                error: errorToJsonValue(error),
            });
        }
    }
    ensureReady() {
        if (this.state !== 'READY') {
            throw new Error(`SyntropyLog is not ready. Current state: '${this.state}'. Ensure init() has completed successfully by listening for the 'ready' event.`);
        }
    }
}

/**
 * @file src/SyntropyLog.ts
 * @description The main public-facing singleton class for the SyntropyLog framework.
 * This class acts as a Facade, providing a simple and clean API surface
 * while delegating all complex lifecycle and orchestration work to the internal
 * LifecycleManager.
 */
/**
 * @class SyntropyLog
 * @description The main public entry point for the framework. It follows the
 * Singleton pattern and acts as an EventEmitter to report on its lifecycle,
 * proxying events from its internal LifecycleManager.
 */
class SyntropyLog extends EventEmitter {
    constructor() {
        super();
        this.lifecycleManager = new LifecycleManager(this);
        // Proxy events from the lifecycle manager to the public facade
        this.lifecycleManager.on('ready', () => this.emit('ready'));
        this.lifecycleManager.on('error', (err) => this.emit('error', err));
        this.lifecycleManager.on('shutting_down', () => this.emit('shutting_down'));
        this.lifecycleManager.on('shutdown', () => this.emit('shutdown'));
    }
    static getInstance() {
        if (!SyntropyLog.instance) {
            SyntropyLog.instance = new SyntropyLog();
        }
        return SyntropyLog.instance;
    }
    getState() {
        return this.lifecycleManager.getState();
    }
    async init(config) {
        return this.lifecycleManager.init(config);
    }
    async shutdown() {
        return this.lifecycleManager.shutdown();
    }
    getLogger(name = 'default', bindings) {
        if (!this.lifecycleManager.loggerFactory) {
            throw new Error('Logger Factory not available.');
        }
        return this.lifecycleManager.loggerFactory.getLogger(name, bindings);
    }
    async getRedis(name) {
        this.lifecycleManager.ensureReady();
        if (!this.lifecycleManager.redisManager) {
            throw new Error('Redis manager not available. Make sure Redis is configured and redis package is installed.');
        }
        return this.lifecycleManager.redisManager.getInstance(name);
    }
    getHttp(name) {
        this.lifecycleManager.ensureReady();
        return this.lifecycleManager.httpManager.getInstance(name);
    }
    getBroker(name) {
        this.lifecycleManager.ensureReady();
        return this.lifecycleManager.brokerManager.getInstance(name);
    }
    getContextManager() {
        this.lifecycleManager.ensureReady();
        return this.lifecycleManager.contextManager;
    }
    getConfig() {
        this.lifecycleManager.ensureReady();
        return this.lifecycleManager.config;
    }
    getFilteredContext(level) {
        this.lifecycleManager.ensureReady();
        return this.lifecycleManager.contextManager.getFilteredContext(level);
    }
    /**
     * Reconfigures the logging matrix dynamically.
     * This method allows changing which context fields are included in logs
     * without affecting security configurations like masking or log levels.
     * @param matrix The new logging matrix configuration
     */
    reconfigureLoggingMatrix(matrix) {
        this.lifecycleManager.ensureReady();
        this.lifecycleManager.contextManager.reconfigureLoggingMatrix(matrix);
    }
    getMasker() {
        if (!this.lifecycleManager.maskingEngine) {
            throw new Error('MaskingEngine not available.');
        }
        return this.lifecycleManager.maskingEngine;
    }
    getSerializer() {
        if (!this.lifecycleManager.serializerRegistry) {
            throw new Error('SerializerRegistry not available.');
        }
        return this.lifecycleManager.serializerRegistry;
    }
    _resetForTesting() {
        // This needs to re-create the lifecycle manager to properly reset state
        this.lifecycleManager.removeAllListeners();
        this.lifecycleManager = new LifecycleManager(this);
        this.removeAllListeners();
        this.lifecycleManager.on('ready', () => this.emit('ready'));
        this.lifecycleManager.on('error', (err) => this.emit('error', err));
        this.lifecycleManager.on('shutting_down', () => this.emit('shutting_down'));
        this.lifecycleManager.on('shutdown', () => this.emit('shutdown'));
    }
}
/** The singleton instance of the SyntropyLog framework. */
const syntropyLog = SyntropyLog.getInstance();

/**
 * @file src/logger/transports/BaseConsolePrettyTransport.ts
 * @description An abstract base class for console transports that provide colored, human-readable output.
 */
/**
 * @class BaseConsolePrettyTransport
 * @description Provides common functionality for "pretty" console transports,
 * including color handling and console method selection. Subclasses must
 * implement the `formatLogString` method to define the final output format.
 * @extends {Transport}
 */
class BaseConsolePrettyTransport extends Transport {
    constructor(options) {
        super(options);
        // Chalk v4 is used directly, not instantiated.
        this.chalk = chalk;
    }
    /**
     * The core log method. It handles common logic and delegates specific
     * formatting to the subclass.
     * @param {LogEntry} entry - The log entry to process.
     * @returns {Promise<void>}
     */
    async log(entry) {
        if (!this.isLevelEnabled(entry.level)) {
            return;
        }
        // Apply the formatter first if it exists.
        const finalObject = this.formatter ? this.formatter.format(entry) : entry;
        // Let the subclass format the final string.
        const logString = this.formatLogString(finalObject);
        // Select the appropriate console method based on the log level.
        const consoleMethod = this.getConsoleMethod(finalObject.level);
        consoleMethod(logString);
    }
    /**
     * Determines which console method to use based on the log level.
     * @param {LogLevel} level - The log level.
     * @returns {Function} The corresponding console method (e.g., console.log).
     */
    getConsoleMethod(level) {
        switch (level) {
            case 'fatal':
            case 'error':
                return console.error;
            case 'warn':
                return console.warn;
            default:
                return console.log;
        }
    }
}

/**
 * @class PrettyConsoleTransport
 * @description A transport that writes logs to the console in a human-readable, colorful format.
 * Ideal for use in development environments.
 * @extends {BaseConsolePrettyTransport}
 */
class PrettyConsoleTransport extends BaseConsolePrettyTransport {
    /**
     * @constructor
     * @param {TransportOptions} [options] - Options for the transport, such as level or a formatter.
     */
    constructor(options) {
        super(options);
        this.levelColorMap = {
            fatal: this.chalk.bgRed.white.bold,
            error: this.chalk.red.bold,
            warn: this.chalk.yellow.bold,
            info: this.chalk.blue.bold,
            debug: this.chalk.green,
            trace: this.chalk.gray,
        };
    }
    /**
     * Formats the log object into a pretty, human-readable string.
     * @param {LogEntry} logObject - The log object to format.
     * @returns {string} The formatted string.
     */
    formatLogString(logObject) {
        const { timestamp, level, service, message, ...rest } = logObject;
        const colorizer = this.levelColorMap[level] ||
            this.chalk.white;
        // Format the main log line
        const time = this.chalk.gray(new Date(timestamp).toLocaleTimeString());
        const levelString = colorizer(`[${level.toUpperCase()}]`);
        const serviceString = this.chalk.cyan(`(${service})`);
        const messageText = message || '';
        let logString = `${time} ${levelString} ${serviceString}: ${messageText}`;
        // Handle additional metadata, ensuring it's not empty
        const metaKeys = Object.keys(rest);
        if (metaKeys.length > 0) {
            // Use a more subtle color for metadata
            const metaString = this.chalk.gray(JSON.stringify(rest, null, 2));
            logString += `\n${metaString}`;
        }
        return logString;
    }
}

/**
 * @class CompactConsoleTransport
 * A transport that writes logs to the console in a compact, single-line format
 * for metadata, optimized for developer productivity.
 * @extends {BaseConsolePrettyTransport}
 */
class CompactConsoleTransport extends BaseConsolePrettyTransport {
    /**
     * @constructor
     * @param {TransportOptions} [options] - Options for the transport, such as level or a formatter.
     */
    constructor(options) {
        super(options);
        this.levelColorMap = {
            fatal: this.chalk.bgRed.white.bold,
            error: this.chalk.red.bold,
            warn: this.chalk.yellow.bold,
            info: this.chalk.cyan.bold, // Using cyan for better contrast in compact view.
            debug: this.chalk.green,
            trace: this.chalk.gray,
        };
    }
    /**
     * Formats the log object into a compact, human-readable string.
     * @param {LogEntry} logObject - The log object to format.
     * @returns {string} The formatted string.
     */
    formatLogString(logObject) {
        const { timestamp, level, service, message, ...rest } = logObject;
        const colorizer = this.levelColorMap[level] ||
            this.chalk.white;
        const time = this.chalk.gray(new Date(timestamp).toLocaleTimeString());
        const levelString = colorizer(`[${level.toUpperCase()}]`);
        const serviceString = this.chalk.blue(`(${service})`);
        const messageText = message || '';
        let logString = `${time} ${levelString} ${serviceString}: ${messageText}`;
        // Format metadata into a single, compact line.
        const metaKeys = Object.keys(rest);
        if (metaKeys.length > 0) {
            const metaString = metaKeys
                .map((key) => {
                const value = rest[key];
                // Simple stringify for objects/arrays in metadata.
                const formattedValue = typeof value === 'object' && value !== null
                    ? JSON.stringify(value)
                    : value;
                return `${this.chalk.dim(key)}=${this.chalk.gray(formattedValue)}`;
            })
                .join(' ');
            // Append metadata on a new, indented line for clarity.
            logString += `\n  ${this.chalk.dim('└─')} ${metaString}`;
        }
        return logString;
    }
}

/**
 * @class ClassicConsoleTransport
 * A transport that writes logs to the console in a classic single-line format,
 * reminiscent of traditional Java logging frameworks.
 * @extends {BaseConsolePrettyTransport}
 */
class ClassicConsoleTransport extends BaseConsolePrettyTransport {
    /**
     * @constructor
     * @param {TransportOptions} [options] - Options for the transport, such as level or a formatter.
     */
    constructor(options) {
        super(options);
        this.levelColorMap = {
            fatal: this.chalk.bgRed.white.bold,
            error: this.chalk.red.bold,
            warn: this.chalk.yellow.bold,
            info: this.chalk.green, // Using green for info in this style
            debug: this.chalk.blue,
            trace: this.chalk.gray,
        };
    }
    /**
     * @private
     * Formats a date object into a 'YYYY-MM-DD HH:mm:ss' string.
     * @param {string} ts - The ISO timestamp string to format.
     * @returns {string} The formatted timestamp.
     */
    formatTimestamp(ts) {
        const date = new Date(ts);
        const YYYY = date.getFullYear();
        const MM = String(date.getMonth() + 1).padStart(2, '0');
        const DD = String(date.getDate()).padStart(2, '0');
        const HH = String(date.getHours()).padStart(2, '0');
        const min = String(date.getMinutes()).padStart(2, '0');
        const ss = String(date.getSeconds()).padStart(2, '0');
        return `${YYYY}-${MM}-${DD} ${HH}:${min}:${ss}`;
    }
    /**
     * Formats the log object into a classic, single-line string.
     * @param {LogEntry} logObject - The log object to format.
     * @returns {string} The formatted string.
     */
    formatLogString(logObject) {
        const { timestamp, level, service, message, context, ...rest } = logObject;
        const colorizer = this.levelColorMap[level] ||
            this.chalk.white;
        // 1. Format the timestamp.
        const timeStr = this.formatTimestamp(timestamp);
        // 2. Format the level, padded to a fixed width for alignment.
        const levelStr = colorizer(level.toUpperCase().padEnd(5));
        // 3. Format the service name.
        const serviceStr = this.chalk.magenta(`[${service}]`);
        // 4. Combine context, other metadata, and message, then format it.
        const allMeta = {
            ...(context || {}),
            ...rest,
            message,
        };
        const metaKeys = Object.keys(allMeta);
        let metaStr = '';
        if (metaKeys.length > 0) {
            metaStr = this.chalk.dim(' [' +
                metaKeys
                    .map((key) => `${key}=${JSON.stringify(allMeta[key])}`)
                    .join(' ') +
                ']');
        }
        // 5. Assemble the final string.
        const logString = `${timeStr} ${levelStr} ${serviceStr}${metaStr}`;
        return logString;
    }
}

/**
 * @class SpyTransport
 * A transport designed for testing. It captures log entries in memory,
 * allowing you to make assertions on what has been logged.
 * @extends {Transport}
 */
class SpyTransport extends Transport {
    /**
     * @constructor
     * @param {TransportOptions} [options] - Options for the transport, such as level.
     */
    constructor(options) {
        super(options);
        this.entries = [];
    }
    /**
     * Stores the log entry in an in-memory array.
     * @param {LogEntry} entry - The log entry to capture.
     * @returns {Promise<void>}
     */
    async log(entry) {
        this.entries.push(entry);
    }
    /**
     * Returns all log entries captured by this transport.
     * @returns {LogEntry[]} A copy of all captured log entries.
     */
    getEntries() {
        return [...this.entries];
    }
    /**
     * Finds log entries where the properties match the given predicate.
     * Note: This performs a shallow comparison on the entry's properties.
     * @param {Partial<LogEntry> | ((entry: LogEntry) => boolean)} predicate - An object with properties to match or a function that returns true for matching entries.
     * @returns {LogEntry[]} An array of matching log entries.
     */
    findEntries(predicate) {
        if (typeof predicate === 'function') {
            // If the predicate is a function, use it directly with filter.
            return this.entries.filter(predicate);
        }
        // If the predicate is an object, perform a shallow property comparison.
        return this.entries.filter((entry) => {
            return Object.keys(predicate).every((key) => {
                const k = key;
                return predicate[k] === entry[k];
            });
        });
    }
    /**
     * Clears all captured log entries. Call this in your test setup
     * (e.g., `beforeEach`) to ensure test isolation.
     * @returns {void}
     */
    clear() {
        this.entries = [];
    }
    /**
     * Returns the first log entry that was captured.
     * @returns {LogEntry | undefined} The first entry, or undefined if none were captured.
     */
    getFirstEntry() {
        return this.entries[0];
    }
    /**
     * Returns the most recent log entry that was captured.
     * @returns {LogEntry | undefined} The last entry, or undefined if none were captured.
     */
    getLastEntry() {
        return this.entries[this.entries.length - 1];
    }
}

/**
 * MockBrokerAdapter - Framework Agnostic Mock
 *
 * This mock provides a testing-agnostic version of IBrokerAdapter
 * that can be used with both Vitest and Jest without conflicts.
 */
/**
 * Creates a simple agnostic mock function without spy capabilities
 */
class MockBrokerAdapter {
    constructor(spyFn) {
        this.spyFn = null;
        this.errors = new Map();
        this.timeouts = new Map();
        this.spyFn = spyFn || null;
        // Initialize mocks after spyFn is set
        this.connect = this.createMock().mockImplementation(async () => {
            // Check for timeout first
            if (this.timeouts.has('connect')) {
                await new Promise((resolve) => setTimeout(resolve, this.timeouts.get('connect') + 10));
                throw new Error(`Mock broker timed out after ${this.timeouts.get('connect')}ms`);
            }
            // Check for error simulation
            if (this.errors.has('connect')) {
                throw this.errors.get('connect');
            }
            return undefined;
        });
        this.disconnect = this.createMock().mockImplementation(async () => {
            if (this.timeouts.has('disconnect')) {
                await new Promise((resolve) => setTimeout(resolve, this.timeouts.get('disconnect') + 10));
                throw new Error(`Mock broker timed out after ${this.timeouts.get('disconnect')}ms`);
            }
            if (this.errors.has('disconnect')) {
                throw this.errors.get('disconnect');
            }
            return undefined;
        });
        this.publish = this.createMock().mockImplementation(async (topic, message) => {
            if (this.timeouts.has('publish')) {
                await new Promise((resolve) => setTimeout(resolve, this.timeouts.get('publish') + 10));
                throw new Error(`Mock broker timed out after ${this.timeouts.get('publish')}ms`);
            }
            if (this.errors.has('publish')) {
                throw this.errors.get('publish');
            }
            return undefined;
        });
        this.subscribe = this.createMock().mockImplementation(async (topic, handler) => {
            if (this.timeouts.has('subscribe')) {
                await new Promise((resolve) => setTimeout(resolve, this.timeouts.get('subscribe') + 10));
                throw new Error(`Mock broker timed out after ${this.timeouts.get('subscribe')}ms`);
            }
            if (this.errors.has('subscribe')) {
                throw this.errors.get('subscribe');
            }
            return undefined;
        });
        this.setError = this.createMock().mockImplementation((method, error) => {
            this.errors.set(method, error);
        });
        this.setTimeout = this.createMock().mockImplementation((method, timeoutMs) => {
            this.timeouts.set(method, timeoutMs);
        });
        this.reset = this.createMock().mockImplementation(() => {
            this.errors.clear();
            this.timeouts.clear();
            this.connect.mockReset();
            this.disconnect.mockReset();
            this.publish.mockReset();
            this.subscribe.mockReset();
            // Restore default implementations
            this.connect.mockImplementation(async () => undefined);
            this.disconnect.mockImplementation(async () => undefined);
            this.publish.mockImplementation(async () => undefined);
            this.subscribe.mockImplementation(async () => undefined);
        });
    }
    createMock(implementation) {
        if (!this.spyFn) {
            throw new Error(`
🚨 SPY FUNCTION NOT INJECTED! 😡

To use spy functions like toHaveBeenCalled(), toHaveBeenCalledWith(), etc.
YOU MUST inject your spy function in the constructor:

// For Vitest:
const mockBroker = new MockBrokerAdapter(vi.fn);

// For Jest:
const mockBroker = new MockBrokerAdapter(jest.fn);

// For Jasmine:
const mockBroker = new MockBrokerAdapter(jasmine.createSpy);

// Without spy (basic functionality only):
const mockBroker = new MockBrokerAdapter();

DON'T FORGET AGAIN! 😤
      `);
        }
        return this.spyFn(implementation);
    }
}

/**
 * MockHttpClient - Framework Agnostic Mock
 *
 * This mock provides a testing-agnostic version of IHttpClientAdapter
 * that can be used with both Vitest and Jest without conflicts.
 */
/**
 * Creates a simple agnostic mock function without spy capabilities
 */
class MockHttpClient {
    constructor(spyFn) {
        this.spyFn = null;
        this.timeouts = new Map();
        this.spyFn = spyFn || null;
        // Initialize mocks after spyFn is set
        this.request = this.createMock().mockImplementation(async (request) => {
            // Default successful response
            return {
                statusCode: 200,
                data: { message: 'Mock response' },
                headers: { 'content-type': 'application/json' },
            };
        });
        this.get = this.createMock().mockImplementation(async (url, headers) => {
            return this.request({
                url,
                method: 'GET',
                headers: headers || {},
            });
        });
        this.post = this.createMock().mockImplementation(async (url, body, headers) => {
            return this.request({
                url,
                method: 'POST',
                headers: headers || {},
                body,
            });
        });
        this.put = this.createMock().mockImplementation(async (url, body, headers) => {
            return this.request({
                url,
                method: 'PUT',
                headers: headers || {},
                body,
            });
        });
        this.delete = this.createMock().mockImplementation(async (url, headers) => {
            return this.request({
                url,
                method: 'DELETE',
                headers: headers || {},
            });
        });
        this.patch = this.createMock().mockImplementation(async (url, body, headers) => {
            return this.request({
                url,
                method: 'PATCH',
                headers: headers || {},
                body,
            });
        });
        // Initialize method implementations
        this.updateMethodImplementations();
        this.setResponse = this.createMock().mockImplementation((method, response) => {
            // Configure the request method to return the specified response
            this.request.mockImplementation(async (req) => {
                if (req.method.toUpperCase() === method.toUpperCase()) {
                    return response;
                }
                // Default response for other methods
                return {
                    statusCode: 200,
                    data: { message: 'Mock response' },
                    headers: { 'content-type': 'application/json' },
                };
            });
            // Also update individual method implementations
            this.updateMethodImplementations();
        });
        this.setError = this.createMock().mockImplementation((method, error) => {
            // Configure the request method to throw the specified error
            this.request.mockImplementation(async (req) => {
                if (req.method.toUpperCase() === method.toUpperCase()) {
                    const adapterError = {
                        name: error.name,
                        message: error.message,
                        stack: error.stack,
                        request: req,
                        isAdapterError: true,
                    };
                    throw adapterError;
                }
                // Default response for other methods
                return {
                    statusCode: 200,
                    data: { message: 'Mock response' },
                    headers: { 'content-type': 'application/json' },
                };
            });
            // Also update individual method implementations
            this.updateMethodImplementations();
        });
        this.setTimeout = this.createMock().mockImplementation((method, timeoutMs) => {
            this.timeouts.set(method, timeoutMs);
            // Configure the request method to timeout
            this.request.mockImplementation(async (req) => {
                if (req.method.toUpperCase() === method.toUpperCase() &&
                    this.timeouts.has(method)) {
                    await new Promise((resolve) => setTimeout(resolve, this.timeouts.get(method) + 10));
                    throw new Error(`Mock HTTP client timed out after ${this.timeouts.get(method)}ms`);
                }
                // Default response for other methods
                return {
                    statusCode: 200,
                    data: { message: 'Mock response' },
                    headers: { 'content-type': 'application/json' },
                };
            });
            // Also update individual method implementations
            this.updateMethodImplementations();
        });
        this.reset = this.createMock().mockImplementation(() => {
            this.timeouts.clear();
            this.request.mockReset();
            this.get.mockReset();
            this.post.mockReset();
            this.put.mockReset();
            this.delete.mockReset();
            this.patch.mockReset();
            // Restore default implementations
            this.request.mockImplementation(async (request) => {
                return {
                    statusCode: 200,
                    data: { message: 'Mock response' },
                    headers: { 'content-type': 'application/json' },
                };
            });
            this.updateMethodImplementations();
        });
    }
    updateMethodImplementations() {
        this.get.mockImplementation(async (url, headers) => {
            return this.request({
                url,
                method: 'GET',
                headers: headers || {},
            });
        });
        this.post.mockImplementation(async (url, body, headers) => {
            return this.request({
                url,
                method: 'POST',
                headers: headers || {},
                body,
            });
        });
        this.put.mockImplementation(async (url, body, headers) => {
            return this.request({
                url,
                method: 'PUT',
                headers: headers || {},
                body,
            });
        });
        this.delete.mockImplementation(async (url, headers) => {
            return this.request({
                url,
                method: 'DELETE',
                headers: headers || {},
            });
        });
        this.patch.mockImplementation(async (url, body, headers) => {
            return this.request({
                url,
                method: 'PATCH',
                headers: headers || {},
                body,
            });
        });
    }
    createMock(implementation) {
        if (!this.spyFn) {
            throw new Error(`
🚨 SPY FUNCTION NOT INJECTED! 😡

To use spy functions like toHaveBeenCalled(), toHaveBeenCalledWith(), etc.
YOU MUST inject your spy function in the constructor:

// For Vitest:
const mockHttp = new MockHttpClient(vi.fn);

// For Jest:
const mockHttp = new MockHttpClient(jest.fn);

// For Jasmine:
const mockHttp = new MockHttpClient(jasmine.createSpy);

// Without spy (basic functionality only):
const mockHttp = new MockHttpClient();

DON'T FORGET AGAIN! 😤
      `);
        }
        return this.spyFn(implementation);
    }
}

/**
 * MockSerializerRegistry - Framework Agnostic Mock
 *
 * This mock provides a testing-agnostic version of SerializerRegistry
 * that can be used with both Vitest and Jest without conflicts.
 */
/**
 * Creates a simple agnostic mock function without spy capabilities
 */
class MockSerializerRegistry {
    constructor(spyFn) {
        // Internal state to track configured serializers
        this.serializers = new Map();
        this.errorKeys = new Set();
        this.timeoutMs = null;
        this.spyFn = null;
        this.spyFn = spyFn || null;
        // Initialize mocks after spyFn is set
        this.process = this.createMock().mockImplementation(async (meta, logger) => {
            // Check for timeout first
            if (this.timeoutMs !== null) {
                await new Promise((resolve) => setTimeout(resolve, this.timeoutMs + 10));
                throw new Error(`Mock serializer timed out after ${this.timeoutMs}ms.`);
            }
            const processedMeta = { ...meta };
            // Process each field with its configured serializer
            for (const [key, value] of Object.entries(processedMeta)) {
                // Check for error simulation
                if (this.errorKeys.has(key)) {
                    throw new Error(`Mock error for key '${key}'`);
                }
                // Check for serializer
                const serializer = this.serializers.get(key);
                if (serializer) {
                    try {
                        processedMeta[key] = serializer(value);
                    }
                    catch (error) {
                        logger.warn(`Mock serializer for key "${key}" failed.`, {
                            error: error instanceof Error ? error.message : String(error),
                        });
                        processedMeta[key] =
                            `[MOCK_SERIALIZER_ERROR: Failed to process key '${key}']`;
                    }
                }
            }
            return processedMeta;
        });
        this.setSerializer = this.createMock().mockImplementation((key, serializer) => {
            this.serializers.set(key, serializer);
        });
        this.setError = this.createMock().mockImplementation((key, error) => {
            this.errorKeys.add(key);
        });
        this.setTimeout = this.createMock().mockImplementation((timeoutMs) => {
            this.timeoutMs = timeoutMs;
        });
        this.reset = this.createMock().mockImplementation(() => {
            this.serializers.clear();
            this.errorKeys.clear();
            this.timeoutMs = null;
            this.process.mockReset();
            this.process.mockImplementation(async (meta, logger) => {
                return { ...meta };
            });
        });
    }
    createMock(implementation) {
        if (!this.spyFn) {
            throw new Error(`
🚨 SPY FUNCTION NOT INJECTED! 😡

To use spy functions like toHaveBeenCalled(), toHaveBeenCalledWith(), etc.
YOU MUST inject your spy function in the constructor:

// For Vitest:
const mockSerializer = new MockSerializerRegistry(vi.fn);

// For Jest:
const mockSerializer = new MockSerializerRegistry(jest.fn);

// For Jasmine:
const mockSerializer = new MockSerializerRegistry(jasmine.createSpy);

// Without spy (basic functionality only):
const mockSerializer = new MockSerializerRegistry();

DON'T FORGET AGAIN! 😤
      `);
        }
        return this.spyFn(implementation);
    }
}

/**
 * FILE: src/redis/RedisConnectionManager.ts
 * DESCRIPTION: Manages the lifecycle of the Redis client connection.
 */
// Type guard for single-node RedisClientType
function isRedisClientType(client) {
    return (typeof client.ping === 'function' &&
        !('commands' in client));
}
/**
 * @class RedisConnectionManager
 * Handles the state and lifecycle of a single native `node-redis` client.
 * It abstracts away the complexities of connection states, retries, and events,
 * providing a stable and predictable promise-based interface for connecting and disconnecting.
 */
class RedisConnectionManager {
    /**
     * Constructs a new RedisConnectionManager.
     * @param {RedisClientOptions | RedisClusterOptions} options - The configuration options for the native `redis` client.
     * @param {ILogger} logger - The logger instance for logging connection events.
     */
    constructor(config, logger) {
        this.connectionPromise = null;
        this.connectionResolve = null;
        this.connectionReject = null;
        this.isConnectedAndReadyState = false;
        this.isQuitState = false;
        this.logger = logger;
        this.instanceName = config.instanceName;
        this.client = this.createNativeClient(config);
        this.setupListeners();
    }
    /**
     * Creates a native Redis client based on the instance configuration mode.
     * @param config The configuration for the specific Redis instance.
     * @returns A `NodeRedisClient` (either single-node or cluster).
     */
    createNativeClient(config) {
        switch (config.mode) {
            case 'single':
            case 'sentinel': {
                // The reconnection strategy only applies to 'single' and 'sentinel' modes.
                // It is defined here so TypeScript can correctly infer that 'config' has the 'retryOptions' property.
                const reconnectStrategy = (retries) => {
                    const maxRetries = config.retryOptions?.maxRetries ?? 10;
                    if (retries > maxRetries) {
                        return new Error('Exceeded the maximum number of Redis connection retries.');
                    }
                    return Math.min(retries * 50, config.retryOptions?.retryDelay ?? 2000);
                };
                if (config.mode === 'single') {
                    return createClient({
                        url: config.url,
                        socket: {
                            reconnectStrategy,
                        },
                    });
                }
                else {
                    // An intermediate variable is created so that TypeScript correctly infers the overload.
                    const sentinelOptions = {
                        sentinels: config.sentinels,
                        name: config.name,
                        sentinelPassword: config.sentinelPassword,
                        socket: {
                            reconnectStrategy,
                        },
                    };
                    return createClient(sentinelOptions);
                }
            }
            case 'cluster': {
                // Reconnection in cluster mode is handled internally by the library.
                // The variable is explicitly typed so that TypeScript uses the correct overload of `createClient`.
                const clusterOptions = {
                    // Transforms the node configuration to the structure expected by the 'redis' library.
                    rootNodes: config.rootNodes.map((node) => ({
                        socket: { host: node.host, port: node.port },
                    })),
                };
                return createClient(clusterOptions);
            }
            default: {
                const _exhaustiveCheck = config;
                throw new Error(`Unsupported Redis mode: "${_exhaustiveCheck.mode}"`); // NOSONAR
            }
        }
    }
    /**
     * Sets up all the necessary event listeners on the native Redis client
     * to manage and report on the connection's lifecycle state.
     * @private
     */
    setupListeners() {
        this.client.on('connect', () => this.logger.info(`Connection established.`));
        this.client.on('ready', () => {
            this.logger.info(`Client is ready.`);
            this.isConnectedAndReadyState = true;
            if (this.connectionResolve) {
                this.connectionResolve();
                this.connectionResolve = null;
                this.connectionReject = null;
            }
        });
        this.client.on('end', () => {
            this.logger.warn(`Connection closed.`);
            this.isConnectedAndReadyState = false;
        });
        this.client.on('error', (err) => {
            this.logger.error(`Client Error.`, { error: err });
            if (this.connectionReject) {
                this.connectionReject(err);
                this.connectionPromise = null;
                this.connectionResolve = null;
                this.connectionReject = null;
            }
        });
        this.client.on('reconnecting', () => {
            this.logger.info(`Client is reconnecting...`);
        });
    }
    /**
     * Initiates a connection to the Redis server.
     * This method is idempotent; it will not attempt to reconnect if already connected
     * or if a connection attempt is already in progress.
     * @returns {Promise<void>} A promise that resolves when the client is connected and ready, or rejects on a connection error.
     */
    connect() {
        if (this.isQuitState) {
            return Promise.reject(new Error('Client has been quit and cannot be reconnected.'));
        }
        if (this.isReady()) {
            return Promise.resolve();
        }
        if (this.connectionPromise) {
            return this.connectionPromise;
        }
        this.logger.info(`Attempting to connect...`);
        this.connectionPromise = new Promise((resolve, reject) => {
            this.connectionResolve = resolve;
            this.connectionReject = reject;
            this.client.connect().catch((err) => {
                this.logger.error(`Immediate connection attempt failed.`, {
                    error: err,
                });
                if (this.connectionReject) {
                    this.connectionReject(err);
                    this.connectionPromise = null;
                    this.connectionResolve = null;
                    this.connectionReject = null;
                }
            });
        });
        return this.connectionPromise;
    }
    /**
     * Ensures the client is connected and ready before proceeding.
     * This is the primary method that should be awaited before executing a command.
     * @returns {Promise<void>} A promise that resolves when the client is ready, or rejects if it can't connect.
     */
    ensureReady() {
        if (this.isQuitState) {
            return Promise.reject(new Error('Client has been quit. Cannot execute commands.'));
        }
        if (!this.isReady() && !this.connectionPromise) {
            this.logger.debug('ensureReady: Client not open, initiating connect.');
        }
        return this.connect();
    }
    /**
     * Gracefully closes the connection to the Redis server by calling `quit()`.
     * It also sets an internal state to prevent any further operations or reconnections.
     * @returns {Promise<void>} A promise that resolves when the client has been successfully quit.
     */
    async disconnect() {
        if (this.isQuitState) {
            this.logger.info('Quit already called. No action taken.');
            return;
        }
        if (this.connectionReject) {
            this.connectionReject(new Error('Connection aborted due to disconnect call.'));
            this.connectionPromise = null;
            this.connectionResolve = null;
            this.connectionReject = null;
        }
        this.isQuitState = true;
        this.isConnectedAndReadyState = false;
        if (this.client.isOpen) {
            this.logger.info('Attempting to quit client.');
            try {
                await this.client.quit();
            }
            catch (error) {
                this.logger.error('Error during client.quit().', { error });
                throw error;
            }
        }
        else {
            this.logger.info('Client was not open. Quit operation effectively complete.');
        }
    }
    /**
     * Retrieves the underlying native `node-redis` client instance.
     * @returns {NodeRedisClient} The native client instance.
     */
    getNativeClient() {
        return this.client;
    }
    /**
     * Checks if the client is currently connected and ready to accept commands.
     * @returns {boolean} `true` if the client is ready, `false` otherwise.
     */
    isReady() {
        return this.isConnectedAndReadyState;
    }
    /**
     * Performs a health check by sending a PING command to the server.
     * @returns {Promise<boolean>} A promise that resolves to `true` if the server responds correctly, `false` otherwise.
     */
    async isHealthy() {
        if (this.isQuitState || !this.isReady()) {
            return false;
        }
        try {
            // By calling this.ping(), we reuse the logic that correctly handles
            // single-node and cluster clients.
            const pong = await this.ping();
            this.logger.debug(`PING response: ${pong}`);
            return pong === 'PONG';
        }
        catch (error) {
            this.logger.error(`PING failed during health check.`, { error });
            return false;
        }
    }
    /**
     * Checks if the disconnect (`quit`) process has been initiated for this client.
     * @returns {boolean} `true` if `disconnect` has been called, `false` otherwise.
     */
    isQuit() {
        return this.isQuitState;
    }
    /**
     * Executes the Redis PING command.
     * Provides a fallback for cluster mode, as PING is not a standard cluster command.
     */
    async ping(message) {
        // First, we ensure the client is ready to receive commands.
        await this.ensureReady();
        // We use the type guard to check if it's a single-node or sentinel client.
        if (isRedisClientType(this.client)) {
            return this.client.ping(message);
        }
        // If it's a cluster client, we simulate the response as the library does.
        return Promise.resolve(message || 'PONG');
    }
    /**
     * Executes the Redis INFO command.
     * Provides a fallback for cluster mode.
     */
    async info(section) {
        // We ensure the client is ready.
        await this.ensureReady();
        // Again, we use the type guard.
        if (isRedisClientType(this.client)) {
            return this.client.info(section);
        }
        // The INFO command does not exist in cluster mode.
        return Promise.resolve('# INFO command is not supported in cluster mode.');
    }
    /**
     * Executes the Redis EXISTS command.
     * @param {string | string[]} keys - A single key or an array of keys to check.
     * @returns {Promise<number>} A promise that resolves with the number of existing keys.
     */
    async exists(keys) {
        await this.ensureReady();
        // The .exists() command is supported by both single-node and cluster clients.
        return this.client.exists(keys);
    }
    /**
     * Executes the Redis GET command.
     * @param {string} key - The key to retrieve.
     * @returns {Promise<string | null>} A promise that resolves with the value or null if not found.
     */
    async get(key) {
        await this.ensureReady();
        return this.client.get(key);
    }
    /**
     * Executes the Redis SET command.
     * @param {string} key - The key to set.
     * @param {string} value - The value to set.
     * @param {number} [ttl] - Optional TTL in seconds.
     * @returns {Promise<string>} A promise that resolves with 'OK' on success.
     */
    async set(key, value, ttl) {
        await this.ensureReady();
        if (ttl) {
            return this.client.setEx(key, ttl, value);
        }
        const result = await this.client.set(key, value);
        return result || 'OK';
    }
    /**
     * Executes the Redis DEL command.
     * @param {string} key - The key to delete.
     * @returns {Promise<number>} A promise that resolves with the number of keys deleted.
     */
    async del(key) {
        await this.ensureReady();
        return this.client.del(key);
    }
}

/**
 * @file src/redis/BeaconRedis.ts
 * @description Implementation of IBeaconRedis that wraps a native `redis` client.
 * It centralizes command execution to add instrumentation (logging, metrics, etc.).
 */
/**
 * The primary implementation of the `IBeaconRedis` interface.
 * This class wraps a native `redis` client and uses a central logger
 * to provide instrumentation for all commands. It delegates connection
 * management and command execution to specialized classes.
 * @implements {IBeaconRedis}
 */
class BeaconRedis {
    /**
     * Constructs a new BeaconRedis instance.
     * @param {RedisInstanceConfig} config - The configuration specific to this Redis instance.
     * @param {RedisConnectionManager} connectionManager - The manager for the client's connection lifecycle.
     * @param {RedisCommandExecutor} commandExecutor - The executor for sending commands to Redis.
     * @param {ILogger} logger - The pre-configured logger instance for this client.
     */
    constructor(config, connectionManager, commandExecutor, logger) {
        this.config = config;
        this.logger = logger;
        this.connectionManager = connectionManager;
        this.commandExecutor = commandExecutor;
    }
    // --- Lifecycle and Management Methods ---
    /**
     * @inheritdoc
     */
    getInstanceName() {
        return this.config.instanceName;
    }
    /**
     * @inheritdoc
     */
    async connect() {
        return this.connectionManager.ensureReady();
    }
    /**
     * @inheritdoc
     */
    async quit() {
        return this.connectionManager.disconnect();
    }
    /**
     * @inheritdoc
     */
    updateConfig(newConfig) {
        this.logger.info({ newConfig }, 'Dynamically updating Redis instance configuration...');
        Object.assign(this.config, newConfig);
    }
    /**
     * @inheritdoc
     * @throws {Error} This method is not yet implemented.
     */
    multi() {
        // TODO: Implement a fully instrumented transaction class.
        // This would need a more complex implementation to queue commands and log them on exec().
        throw new Error('The multi() method is not yet implemented.');
    }
    /**
     * A centralized method for executing and instrumenting any Redis command.
     * It ensures the client is ready, executes the command, logs the outcome
     * (success or failure) with timing information, and handles errors.
     * @private
     * @template T The expected return type of the command.
     * @param {string} commandName - The name of the Redis command (e.g., 'GET', 'HSET').
     * @param {() => Promise<T>} commandFn - A function that, when called, executes the native Redis command.
     * @param {...RedisValue[]} params - The parameters passed to the original command, used for logging.
     * @returns {Promise<T>} A promise that resolves with the result of the command.
     * @throws The error from the native command is re-thrown after being logged.
     */
    async _executeCommand(commandName, commandFn, ...params) {
        const startTime = Date.now();
        // Use a base logger with the source pre-set for this command.
        const commandLogger = this.logger.withSource('redis');
        try {
            // 1. Ensure the client is connected and ready before executing.
            await this.connectionManager.ensureReady();
            // 2. Execute the command by calling the provided function.
            const result = await commandFn();
            const durationMs = Date.now() - startTime;
            // 3. On success, log the execution details.
            // Determine the log level from the instance's specific configuration.
            const logLevel = this.config.logging?.onSuccess ?? 'debug';
            const logPayload = {
                command: commandName,
                instance: this.getInstanceName(),
                durationMs,
            };
            // Conditionally add command parameters and return value to the log payload.
            if (this.config.logging?.logCommandValues) {
                logPayload.params = params;
            }
            if (this.config.logging?.logReturnValue) {
                logPayload.result = result;
            }
            // The log is sent to the central pipeline where serialization and masking occur.
            commandLogger[logLevel](logPayload, `Redis command [${commandName}] executed successfully.`);
            return result;
        }
        catch (error) {
            const durationMs = Date.now() - startTime;
            const errorLogLevel = this.config.logging?.onError ?? 'error';
            // The error object will be serialized by the central SerializerRegistry.
            commandLogger[errorLogLevel]({
                command: commandName,
                instance: this.getInstanceName(),
                durationMs,
                err: errorToJsonValue(error),
                params: this.config.logging?.logCommandValues ? params : undefined,
            }, `Redis command [${commandName}] failed.`);
            throw error;
        }
    }
    // --- Public Command Methods ---
    // Each command now simply calls _executeCommand. The structure remains the same.
    /**
     * @inheritdoc
     */
    async get(key) {
        return this._executeCommand('GET', () => this.commandExecutor.get(key), key);
    }
    /**
     * @inheritdoc
     */
    async set(key, value, ttlSeconds) {
        const options = ttlSeconds ? { EX: ttlSeconds } : undefined;
        return this._executeCommand('SET', () => this.commandExecutor.set(key, value, options), key, value, ttlSeconds);
    }
    /**
     * @inheritdoc
     */
    async del(keys) {
        return this._executeCommand('DEL', () => this.commandExecutor.del(keys), keys);
    }
    /**
     * @inheritdoc
     */
    async exists(keys) {
        return this._executeCommand('EXISTS', () => this.commandExecutor.exists(keys), keys);
    }
    /**
     * @inheritdoc
     */
    async expire(key, seconds) {
        return this._executeCommand('EXPIRE', () => this.commandExecutor.expire(key, seconds), key, seconds);
    }
    /**
     * @inheritdoc
     */
    async ttl(key) {
        return this._executeCommand('TTL', () => this.commandExecutor.ttl(key), key);
    }
    /**
     * @inheritdoc
     */
    async incr(key) {
        return this._executeCommand('INCR', () => this.commandExecutor.incr(key), key);
    }
    /**
     * @inheritdoc
     */
    async decr(key) {
        return this._executeCommand('DECR', () => this.commandExecutor.decr(key), key);
    }
    /**
     * @inheritdoc
     */
    async incrBy(key, increment) {
        return this._executeCommand('INCRBY', () => this.commandExecutor.incrBy(key, increment), key, increment);
    }
    /**
     * @inheritdoc
     */
    async decrBy(key, decrement) {
        return this._executeCommand('DECRBY', () => this.commandExecutor.decrBy(key, decrement), key, decrement);
    }
    /**
     * @inheritdoc
     */
    async hGet(key, field) {
        return this._executeCommand('HGET', async () => (await this.commandExecutor.hGet(key, field)) ?? null, key, field);
    }
    async hSet(key, fieldOrFields, value) {
        if (typeof fieldOrFields === 'string') {
            // Handle single field-value pair.
            return this._executeCommand('HSET', () => this.commandExecutor.hSet(key, fieldOrFields, value), key, fieldOrFields, value);
        }
        // Handle object of field-value pairs.
        return this._executeCommand('HSET', () => this.commandExecutor.hSet(key, fieldOrFields), key, fieldOrFields);
    }
    /**
     * @inheritdoc
     */
    async hGetAll(key) {
        return this._executeCommand('HGETALL', () => this.commandExecutor.hGetAll(key), key);
    }
    /**
     * @inheritdoc
     */
    async hDel(key, fields) {
        return this._executeCommand('HDEL', () => this.commandExecutor.hDel(key, fields), key, fields);
    }
    /**
     * @inheritdoc
     */
    async hExists(key, field) {
        return this._executeCommand('HEXISTS', () => this.commandExecutor.hExists(key, field), key, field);
    }
    /**
     * @inheritdoc
     */
    async hIncrBy(key, field, increment) {
        return this._executeCommand('HINCRBY', () => this.commandExecutor.hIncrBy(key, field, increment), key, field, increment);
    }
    async lPush(key, elementOrElements) {
        return this._executeCommand('LPUSH', () => this.commandExecutor.lPush(key, elementOrElements), key, elementOrElements);
    }
    async rPush(key, elementOrElements) {
        return this._executeCommand('RPUSH', () => this.commandExecutor.rPush(key, elementOrElements), key, elementOrElements);
    }
    /**
     * @inheritdoc
     */
    async lPop(key) {
        return this._executeCommand('LPOP', () => this.commandExecutor.lPop(key), key);
    }
    /**
     * @inheritdoc
     */
    async rPop(key) {
        return this._executeCommand('RPOP', () => this.commandExecutor.rPop(key), key);
    }
    /**
     * @inheritdoc
     */
    async lRange(key, start, stop) {
        return this._executeCommand('LRANGE', () => this.commandExecutor.lRange(key, start, stop), key, start, stop);
    }
    /**
     * @inheritdoc
     */
    async lLen(key) {
        return this._executeCommand('LLEN', () => this.commandExecutor.lLen(key), key);
    }
    /**
     * @inheritdoc
     */
    async lTrim(key, start, stop) {
        return this._executeCommand('LTRIM', () => this.commandExecutor.lTrim(key, start, stop), key, start, stop);
    }
    async sAdd(key, memberOrMembers) {
        return this._executeCommand('SADD', () => this.commandExecutor.sAdd(key, memberOrMembers), key, memberOrMembers);
    }
    /**
     * @inheritdoc
     */
    async sMembers(key) {
        return this._executeCommand('SMEMBERS', () => this.commandExecutor.sMembers(key), key);
    }
    /**
     * @inheritdoc
     */
    async sIsMember(key, member) {
        return this._executeCommand('SISMEMBER', () => this.commandExecutor.sIsMember(key, member), key, member);
    }
    async sRem(key, memberOrMembers) {
        return this._executeCommand('SREM', () => this.commandExecutor.sRem(key, memberOrMembers), key, memberOrMembers);
    }
    /**
     * @inheritdoc
     */
    async sCard(key) {
        return this._executeCommand('SCARD', () => this.commandExecutor.sCard(key), key);
    }
    async zAdd(key, scoreOrMembers, member) {
        // Check if we are using the array overload for multiple members.
        if (Array.isArray(scoreOrMembers)) {
            return this._executeCommand('ZADD', () => this.commandExecutor.zAdd(key, scoreOrMembers), key, scoreOrMembers);
        }
        // Handle single score-member pair.
        return this._executeCommand('ZADD', () => this.commandExecutor.zAdd(key, scoreOrMembers, member), key, scoreOrMembers, member);
    }
    /**
     * @inheritdoc
     */
    async zRange(key, min, max, options) {
        return this._executeCommand('ZRANGE', () => this.commandExecutor.zRange(key, min, max, options), key, min, max, options);
    }
    /**
     * @inheritdoc
     */
    async zRangeWithScores(key, min, max, options) {
        return this._executeCommand('ZRANGE_WITHSCORES', () => this.commandExecutor.zRangeWithScores(key, min, max, options), key, min, max, options);
    }
    /**
     * @inheritdoc
     */
    async zRem(key, members) {
        return this._executeCommand('ZREM', () => this.commandExecutor.zRem(key, members), key, members);
    }
    /**
     * @inheritdoc
     */
    async zCard(key) {
        return this._executeCommand('ZCARD', () => this.commandExecutor.zCard(key), key);
    }
    /**
     * @inheritdoc
     */
    async zScore(key, member) {
        return this._executeCommand('ZSCORE', () => this.commandExecutor.zScore(key, member), key, member);
    }
    /**
     * Subscribes the client to a channel to listen for messages.
     * Note: This is a long-lived command. The initial subscription action is logged,
     * but individual messages received by the listener are not logged by this wrapper.
     * The listener itself should handle any required logging for received messages.
     * @param {string} channel - The channel to subscribe to.
     * @param {(message: string, channel: string) => void} listener - The function to call when a message is received.
     * @returns {Promise<void>} A promise that resolves when the subscription is successful.
     */
    async subscribe(channel, listener) {
        return this._executeCommand('SUBSCRIBE', () => this.commandExecutor.subscribe(channel, listener), channel);
    }
    /**
     * Unsubscribes the client from a channel, or all channels if none is specified.
     * @param {string} [channel] - The optional channel to unsubscribe from.
     * @returns {Promise<void>} A promise that resolves when the unsubscription is successful.
     */
    async unsubscribe(channel) {
        return this._executeCommand('UNSUBSCRIBE', () => this.commandExecutor.unsubscribe(channel), channel);
    }
    /**
     * @inheritdoc
     */
    async ping(message) {
        return this._executeCommand('PING', () => this.connectionManager.ping(message), message);
    }
    /**
     * @inheritdoc
     */
    async info(section) {
        return this._executeCommand('INFO', () => this.connectionManager.info(section), section);
    }
    /**
     * Executes a Lua script on the server.
     * @param {string} script - The Lua script to execute.
     * @param {string[]} keys - An array of key names used by the script, accessible via the `KEYS` table in Lua.
     * @param {string[]} args - An array of argument values for the script, accessible via the `ARGV` table in Lua.
     * @returns {Promise<any>} A promise that resolves with the result of the script execution.
     */
    async eval(script, keys, args) {
        return this._executeCommand('EVAL', () => this.commandExecutor.eval(script, keys, args), script, keys, args);
    }
}

/**
 * @file src/redis/RedisCommandExecutor.ts
 * @description A thin wrapper around the native `node-redis` client that directly executes commands.
 * This class's sole responsibility is to pass commands to the underlying client.
 * It does not contain any logic for instrumentation, connection management, or error handling.
 */
/**
 * Executes Redis commands against a native `node-redis` client.
 * This class acts as a direct pass-through to the client's methods,
 * decoupling the command execution from the instrumentation and connection logic.
 */
class RedisCommandExecutor {
    /**
     * Constructs a new RedisCommandExecutor.
     * @param {NodeRedisClient} client The native `node-redis` client (single-node or cluster) to execute commands on.
     */
    constructor(client) {
        this.client = client;
    }
    // --- String Commands ---
    /**
     * Executes the native GET command.
     * @param {string} key The key to retrieve.
     * @returns {Promise<string | null>} The value of the key, or null if it does not exist.
     */
    get(key) {
        return this.client.get(key);
    }
    /**
     * Executes the native SET command.
     * @param {string} key The key to set.
     * @param {string} value The value to set.
     * @param {RedisCommandOptions} [options] Optional SET options (e.g., EX, NX).
     * @returns {Promise<string | null>} 'OK' if successful, or null.
     */
    set(key, value, options) {
        return this.client.set(key, value, options);
    }
    /**
     * Executes the native DEL command.
     * @param {string | string[]} keys The key or keys to delete.
     * @returns {Promise<number>} The number of keys deleted.
     */
    del(keys) {
        return this.client.del(keys);
    }
    /**
     * Executes the native EXISTS command.
     * @param {string | string[]} keys The key or keys to check.
     * @returns {Promise<number>} The number of keys that exist.
     */
    exists(keys) {
        return this.client.exists(keys);
    }
    /**
     * Executes the native EXPIRE command.
     * @param {string} key The key to set the expiration for.
     * @param {number} seconds The time-to-live in seconds.
     * @returns {Promise<boolean>} True if the timeout was set, false otherwise.
     */
    expire(key, seconds) {
        return this.client.expire(key, seconds);
    }
    /**
     * Executes the native TTL command.
     * @param {string} key The key to check.
     * @returns {Promise<number>} The remaining time to live in seconds.
     */
    ttl(key) {
        return this.client.ttl(key);
    }
    /**
     * Executes the native INCR command.
     * @param {string} key The key to increment.
     * @returns {Promise<number>} The value after the increment.
     */
    incr(key) {
        return this.client.incr(key);
    }
    /**
     * Executes the native DECR command.
     * @param {string} key The key to decrement.
     * @returns {Promise<number>} The value after the decrement.
     */
    decr(key) {
        return this.client.decr(key);
    }
    /**
     * Executes the native INCRBY command.
     * @param {string} key The key to increment.
     * @param {number} increment The amount to increment by.
     * @returns {Promise<number>} The value after the increment.
     */
    incrBy(key, increment) {
        return this.client.incrBy(key, increment);
    }
    /**
     * Executes the native DECRBY command.
     * @param {string} key The key to decrement.
     * @param {number} decrement The amount to decrement by.
     * @returns {Promise<number>} The value after the decrement.
     */
    decrBy(key, decrement) {
        return this.client.decrBy(key, decrement);
    }
    // --- Hash Commands ---
    /**
     * Executes the native HGET command.
     * @param {string} key The key of the hash.
     * @param {string} field The field to retrieve.
     * @returns {Promise<string | undefined>} The value of the field, or undefined if it does not exist.
     */
    hGet(key, field) {
        return this.client.hGet(key, field);
    }
    /**
     * Executes the native HSET command.
     * @param {string} key The key of the hash.
     * @param {string | Record<string, RedisHashValue>} fieldOrFields The field to set or an object of field-value pairs.
     * @param {RedisHashValue} [value] The value to set if a single field is provided.
     * @returns {Promise<number>} The number of fields that were added.
     */
    hSet(key, fieldOrFields, value) {
        if (typeof fieldOrFields === 'string') {
            return this.client.hSet(key, fieldOrFields, value);
        }
        // When fieldOrFields is an object, call the two-argument overload.
        return this.client.hSet(key, fieldOrFields);
    }
    /**
     * Executes the native HGETALL command.
     * @param {string} key The key of the hash.
     * @returns {Promise<Record<string, string>>} An object containing all fields and values.
     */
    hGetAll(key) {
        return this.client.hGetAll(key);
    }
    /**
     * Executes the native HDEL command.
     * @param {string} key The key of the hash.
     * @param {string | string[]} fields The field or fields to delete.
     * @returns {Promise<number>} The number of fields that were removed.
     */
    hDel(key, fields) {
        return this.client.hDel(key, fields);
    }
    /**
     * Executes the native HEXISTS command.
     * @param {string} key The key of the hash.
     * @param {string} field The field to check.
     * @returns {Promise<boolean>} True if the field exists, false otherwise.
     */
    hExists(key, field) {
        return this.client.hExists(key, field);
    }
    /**
     * Executes the native HINCRBY command.
     * @param {string} key The key of the hash.
     * @param {string} field The field to increment.
     * @param {number} increment The amount to increment by.
     * @returns {Promise<number>} The value of the field after the increment.
     */
    hIncrBy(key, field, increment) {
        return this.client.hIncrBy(key, field, increment);
    }
    // --- List Commands ---
    /**
     * Executes the native LPUSH command.
     * @param {string} key The key of the list.
     * @param {RedisListElement | RedisListElement[]} elements The element or elements to prepend.
     * @returns {Promise<number>} The length of the list after the operation.
     */
    lPush(key, elements) {
        return this.client.lPush(key, elements);
    }
    /**
     * Executes the native RPUSH command.
     * @param {string} key The key of the list.
     * @param {RedisListElement | RedisListElement[]} elements The element or elements to append.
     * @returns {Promise<number>} The length of the list after the operation.
     */
    rPush(key, elements) {
        return this.client.rPush(key, elements);
    }
    /**
     * Executes the native LPOP command.
     * @param {string} key The key of the list.
     * @returns {Promise<string | null>} The value of the first element, or null if the list is empty.
     */
    lPop(key) {
        return this.client.lPop(key);
    }
    /**
     * Executes the native RPOP command.
     * @param {string} key The key of the list.
     * @returns {Promise<string | null>} The value of the last element, or null if the list is empty.
     */
    rPop(key) {
        return this.client.rPop(key);
    }
    /**
     * Executes the native LRANGE command.
     * @param {string} key The key of the list.
     * @param {number} start The starting index.
     * @param {number} stop The ending index.
     * @returns {Promise<string[]>} An array of elements in the specified range.
     */
    lRange(key, start, stop) {
        return this.client.lRange(key, start, stop);
    }
    /**
     * Executes the native LLEN command.
     * @param {string} key The key of the list.
     * @returns {Promise<number>} The length of the list.
     */
    lLen(key) {
        return this.client.lLen(key);
    }
    /**
     * Executes the native LTRIM command.
     * @param {string} key The key of the list.
     * @param {number} start The starting index.
     * @param {number} stop The ending index.
     * @returns {Promise<string>} 'OK'.
     */
    lTrim(key, start, stop) {
        return this.client.lTrim(key, start, stop);
    }
    // --- Set Commands ---
    /**
     * Executes the native SADD command.
     * @param {string} key The key of the set.
     * @param {RedisSetMember | RedisSetMember[]} members The member or members to add.
     * @returns {Promise<number>} The number of members added to the set.
     */
    sAdd(key, members) {
        return this.client.sAdd(key, members);
    }
    /**
     * Executes the native SMEMBERS command.
     * @param {string} key The key of the set.
     * @returns {Promise<string[]>} An array of all members in the set.
     */
    sMembers(key) {
        return this.client.sMembers(key);
    }
    /**
     * Executes the native SISMEMBER command.
     * @param {string} key The key of the set.
     * @param {RedisSetMember} member The member to check for.
     * @returns {Promise<boolean>} True if the member is in the set, false otherwise.
     */
    sIsMember(key, member) {
        return this.client.sIsMember(key, member);
    }
    /**
     * Executes the native SREM command.
     * @param {string} key The key of the set.
     * @param {RedisSetMember | RedisSetMember[]} members The member or members to remove.
     * @returns {Promise<number>} The number of members removed from the set.
     */
    sRem(key, members) {
        return this.client.sRem(key, members);
    }
    /**
     * Executes the native SCARD command.
     * @param {string} key The key of the set.
     * @returns {Promise<number>} The cardinality of the set.
     */
    sCard(key) {
        return this.client.sCard(key);
    }
    // --- Sorted Set Commands ---
    /**
     * Executes the native ZADD command.
     * @param {string} key The key of the sorted set.
     * @param {number | RedisSortedSetMember[]} scoreOrMembers The score for a single member, or an array of member-score objects.
     * @param {RedisValue} [member] The member to add if a single score is provided.
     * @returns {Promise<number>} The number of elements added to the sorted set.
     */
    zAdd(key, scoreOrMembers, member) {
        if (Array.isArray(scoreOrMembers)) {
            return this.client.zAdd(key, scoreOrMembers);
        }
        // For a single member, the native client expects a ZMember object or an array of them.
        return this.client.zAdd(key, {
            score: scoreOrMembers,
            value: member,
        });
    }
    /**
     * Executes the native ZRANGE command.
     * @param {string} key The key of the sorted set.
     * @param {string | number} min The minimum index or score.
     * @param {string | number} max The maximum index or score.
     * @param {RedisCommandOptions} [options] Additional options (e.g., REV).
     * @returns {Promise<string[]>} An array of members in the specified range.
     */
    zRange(key, min, max, options) {
        return this.client.zRange(key, min, max, options);
    }
    /**
     * Executes the native ZRANGE command with the WITHSCORES option.
     * @param {string} key The key of the sorted set.
     * @param {string | number} min The minimum index or score.
     * @param {string | number} max The maximum index or score.
     * @param {RedisCommandOptions} [options] Additional options (e.g., REV).
     * @returns {Promise<RedisZMember[]>} An array of members and their scores.
     */
    zRangeWithScores(key, min, max, options) {
        return this.client.zRangeWithScores(key, min, max, options);
    }
    /**
     * Executes the native ZREM command.
     * @param {string} key The key of the sorted set.
     * @param {RedisValue | RedisValue[]} members The member or members to remove.
     * @returns {Promise<number>} The number of members removed.
     */
    zRem(key, members) {
        return this.client.zRem(key, members);
    }
    /**
     * Executes the native ZCARD command.
     * @param {string} key The key of the sorted set.
     * @returns {Promise<number>} The cardinality of the sorted set.
     */
    zCard(key) {
        return this.client.zCard(key);
    }
    /**
     * Executes the native ZSCORE command.
     * @param {string} key The key of the sorted set.
     * @param {RedisValue} member The member whose score to retrieve.
     * @returns {Promise<number | null>} The score of the member, or null if it does not exist.
     */
    zScore(key, member) {
        return this.client.zScore(key, member);
    }
    // --- Scripting and Pub/Sub Commands ---
    /**
     * Executes the native EVAL command.
     * @param {string} script The Lua script to execute.
     * @param {string[]} keys An array of key names.
     * @param {string[]} args An array of argument values.
     * @returns {Promise<any>} The result of the script execution.
     */
    eval(script, keys, args) {
        return this.client.eval(script, { keys, arguments: args });
    }
    /**
     * Executes the native SUBSCRIBE command.
     * @param {string} channel The channel to subscribe to.
     * @param {(message: string, channel: string) => void} listener The callback for received messages.
     * @returns {Promise<void>}
     */
    subscribe(channel, listener) {
        return this.client.subscribe(channel, listener);
    }
    /**
     * Executes the native UNSUBSCRIBE command.
     * @param {string} [channel] The channel to unsubscribe from. If omitted, unsubscribes from all.
     * @returns {Promise<void>}
     */
    unsubscribe(channel) {
        if (channel) {
            return this.client.unsubscribe(channel);
        }
        return this.client.unsubscribe();
    }
    /**
     * Executes the native PUBLISH command.
     * @param {string} channel The channel to publish to.
     * @param {string} message The message to publish.
     * @returns {Promise<number>} The number of clients that received the message.
     */
    publish(channel, message) {
        return this.client.publish(channel, message);
    }
}

/**
 * @file src/redis/RedisManager.ts
 * @description Manages the lifecycle of multiple instrumented Redis client instances.
 */
/**
 * Manages the creation, retrieval, and lifecycle of multiple `IBeaconRedis` instances
 * based on the provided configuration. It acts as a central point of access for all
 * Redis clients within an application.
 */
class RedisManager {
    constructor(config, logger, contextManager) {
        this.instances = new Map();
        this.config = config;
        this.logger = logger.child({ module: 'RedisManager' });
        this.contextManager = contextManager;
    }
    init() {
        this.logger.trace('Initializing RedisManager...');
        // If no instances are configured, just log and return
        if (!this.config ||
            !this.config.instances ||
            this.config.instances.length === 0) {
            this.logger.trace('No Redis instances to initialize.');
            return;
        }
        // Functional validation: Check if configuration is valid
        const validateConfig = () => {
            if (!this.config ||
                !this.config.instances ||
                this.config.instances.length === 0) {
                throw new Error('Redis configuration is invalid: no instances configured. Please provide at least one Redis instance.');
            }
        };
        // Functional validation: Check if default instance exists (if specified)
        const validateDefaultInstance = () => {
            if (this.config.default) {
                const defaultExists = this.config.instances.some((instance) => instance.instanceName === this.config.default);
                if (!defaultExists) {
                    throw new Error(`Redis configuration error: default instance "${this.config.default}" not found in configured instances. Available instances: ${this.config.instances.map((i) => i.instanceName).join(', ')}`);
                }
            }
        };
        // Functional instance creation with BeaconRedis
        const createInstances = () => {
            for (const instanceConfig of this.config.instances) {
                // Create connection manager
                const connectionManager = new RedisConnectionManager(instanceConfig, this.logger);
                // Create command executor
                const commandExecutor = new RedisCommandExecutor(connectionManager.getNativeClient());
                // Create instrumented BeaconRedis instance
                const beaconRedis = new BeaconRedis(instanceConfig, connectionManager, commandExecutor, this.logger);
                this.instances.set(instanceConfig.instanceName, beaconRedis);
                if (instanceConfig.instanceName === this.config.default) {
                    this.defaultInstance = beaconRedis;
                }
            }
        };
        // Functional fallback for default instance
        const setDefaultFallback = () => {
            if (!this.defaultInstance && this.instances.size > 0) {
                const firstInstance = this.instances.values().next().value;
                this.defaultInstance = firstInstance;
            }
        };
        // Execute the functional pipeline
        try {
            validateConfig();
            validateDefaultInstance();
            createInstances();
            setDefaultFallback();
        }
        catch (error) {
            this.logger.error('RedisManager initialization failed', {
                error: error instanceof Error ? error.message : String(error),
            });
            throw error;
        }
    }
    getInstance(name) {
        const instanceName = name ?? this.defaultInstance?.getInstanceName();
        if (!instanceName) {
            throw new Error('A specific instance name was not provided and no default Redis instance is configured.');
        }
        const instance = this.instances.get(instanceName);
        if (!instance) {
            throw new Error(`Redis instance with name "${instanceName}" was not found. Please check that the name is spelled correctly in your configuration and code.`);
        }
        return instance;
    }
    /**
     * Gracefully shuts down all managed Redis connections.
     * It attempts to close all connections and waits for them to complete.
     */
    async shutdown() {
        this.logger.info('Closing all Redis connections...');
        const shutdownPromises = Array.from(this.instances.values()).map((instance) => instance.quit());
        await Promise.allSettled(shutdownPromises);
        this.logger.info('All Redis connections have been closed.');
    }
}

var RedisManager$1 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    RedisManager: RedisManager
});

export { ClassicConsoleTransport, CompactConsoleTransport, ConsoleTransport, MaskingEngine, MockBrokerAdapter, MockHttpClient, MockSerializerRegistry, PrettyConsoleTransport, SanitizationEngine, SpyTransport, SyntropyLog, Transport, syntropyLog };
//# sourceMappingURL=index.mjs.map