/* * This file is part of the xPack project (http://xpack.github.io). * Copyright (c) 2021-2026 Liviu Ionescu. All rights reserved. * * Permission to use, copy, modify, and/or distribute this software * for any purpose is hereby granted, under the terms of the MIT license. * * If a copy of the license was not distributed with this file, it can * be obtained from https://opensource.org/license/mit. */ // ---------------------------------------------------------------------------- import { Logger } from '@xpack/logger' import { ConfigurationError } from './errors.js' // ============================================================================ /** * The default maximum number of combinations that can be generated. * * @remarks * This constant defines the default limit for the total number of * combinations that a {@link CombinationsGenerator} instance can produce. * The limit exists to prevent performance issues and memory exhaustion when * working with large matrices. * * This value can be overridden by providing a custom `maxCombinations` * parameter when constructing a {@link CombinationsGenerator} instance. * * The limit is intentionally low (42 * 10). */ export const COMBINATIONS_GENERATOR_MAX_COMBINATIONS_LIMIT = 42 * 10 /** * A matrix combination mapping parameter names to their values. * * @remarks * This type represents a single combination generated from a matrix of * parameters, where each key is a parameter name and each value is a * string from that parameter's array of possible values. * * Example: `{ arch: 'x64', platform: 'linux', optimize: 'speed' }` */ export type MatrixCombination = Record // ============================================================================ /** * Configuration parameters for constructing a combinations generator instance. * * @remarks * This interface defines the required configuration for creating an * instance of {@link CombinationsGenerator}. All properties are mandatory. * * The parameters provide the matrix parameter names, their corresponding * value arrays for Cartesian product computation, and the logger for * diagnostic output during combination generation. */ export interface CombinationsGeneratorConstructorParameters { /** * The array of parameter names. */ matrixKeys: string[] /** * The array of value arrays for each parameter. */ matrixValues: string[][] /** * The logger instance for output and diagnostics. */ log: Logger /** * Optional maximum combinations limit to prevent excessive generation. * * @remarks * This parameter allows configuring a custom limit on the total number of * combinations that can be generated. If the calculated total exceeds this * limit, a {@link ConfigurationError} is thrown to prevent performance * issues or memory exhaustion. The default value is 10,000 combinations. */ maxCombinations?: number } /** * Generates all possible combinations from a matrix of parameters. * * @remarks * This class computes the Cartesian product of multiple parameter arrays, * producing all possible combinations of parameter values using a * memory-efficient generator pattern. It uses a recursive algorithm to * systematically explore all combinations one at a time. * * The generation process: * *
    *
  1. Takes arrays of parameter names (keys) and their corresponding value * arrays.
  2. *
  3. Recursively iterates through each parameter, selecting one value at a * time.
  4. *
  5. Yields complete combinations one at a time without storing them all * in memory.
  6. *
  7. Backtracks to explore other value combinations.
  8. *
* * Example usage: * ```typescript * const generator = new CombinationsGenerator({ * matrixKeys: ['arch', 'optimize'], * matrixValues: [['x64', 'arm'], ['speed', 'size']], * log * }); * for (const combo of generator.generate()) { * // Process one combination at a time without storing all in memory * // Results: * // { arch: 'x64', optimize: 'speed' } * // { arch: 'x64', optimize: 'size' } * // { arch: 'arm', optimize: 'speed' } * // { arch: 'arm', optimize: 'size' } * await processConfiguration(combo); * } * ``` */ export class CombinationsGenerator { // -------------------------------------------------------------------------- // Protected Members. /** * The logger instance for output and diagnostics. * * @remarks * This logger provides trace-level diagnostics during combination * generation, enabling visibility into the recursive exploration of the * parameter space without impacting performance when tracing is disabled. */ protected readonly _log: Logger /** * The array of parameter names. * * @remarks * This array contains the names of all matrix parameters in the order * they should be processed during combination generation. Each key * corresponds to a parameter that will appear in the generated * combinations. */ protected readonly _matrixKeys: string[] /** * The array of value arrays for each parameter. * * @remarks * This two-dimensional array contains the possible values for each * parameter. The outer array corresponds one-to-one with * matrixKeys, where matrixValues[i] contains * all possible values for the parameter matrixKeys[i]. * * The Cartesian product of all these value arrays produces the complete * set of combinations. */ protected readonly _matrixValues: string[][] /** * The maximum number of combinations allowed. * * @remarks * This limit prevents excessive generation of combinations, protecting * against performance issues and memory exhaustion. */ protected readonly _maxCombinations: number // -------------------------------------------------------------------------- // Constructor. /** * Constructs a combinations generator instance. * * @param matrixKeys - The array of parameter names. * @param matrixValues - The array of value arrays for each parameter. * @param log - The logger instance for output and diagnostics. * * @remarks * The constructor validates that the structure of keys and values is * correct and prepares the generator for combination generation. The * actual generation is performed by calling `*generate`. */ constructor({ matrixKeys, matrixValues, maxCombinations = COMBINATIONS_GENERATOR_MAX_COMBINATIONS_LIMIT, log, }: CombinationsGeneratorConstructorParameters) { this._log = log this._matrixKeys = matrixKeys this._matrixValues = matrixValues this._maxCombinations = maxCombinations log.trace( `${CombinationsGenerator.name}.constructor: ` + `matrixKeys=${JSON.stringify(this._matrixKeys)} ` + `matrixValues=${JSON.stringify(this._matrixValues)}` ) } // -------------------------------------------------------------------------- // Public Methods. /** * Generates combinations one at a time using a generator pattern. * * @remarks * This method yields combinations one at a time instead of building the * entire array in memory. This is particularly useful for large matrices * where storing all combinations would be impractical. * * Memory efficiency: * * * * Example usage: * ```typescript * for (const combo of generator.generate()) { * // Process one combination at a time * await processConfiguration(combo) * } * ``` * * Yields: Individual matrix combinations one at a time. */ *generate(): Generator { // Empty matrix produces no combinations if (this._matrixKeys.length === 0) { return } // Calculate total combinations const totalCombinations = this._matrixValues.reduce( (product, values) => product * values.length, 1 ) if (totalCombinations > this._maxCombinations) { throw new ConfigurationError( `Matrix would generate ${String(totalCombinations)} combinations, ` + `exceeding limit of ${String(this._maxCombinations)}. ` + `Consider using fewer parameters or values.` ) } yield* this._generateRecursively(0, {}) } // -------------------------------------------------------------------------- // Private Methods. /** * Recursively generates combinations as a generator, yielding one at * a time. * * @remarks * This method implements the recursive algorithm for generating the * Cartesian product of parameter values using the generator pattern. * * Algorithm steps: * *
    *
  1. Base case: If all parameters have been assigned values * (index === matrixKeys.length), yield a copy of the * current combination and return.
  2. *
  3. Recursive case: For the parameter at the current index: *
      *
    • Iterate through all possible values for this parameter.
    • *
    • Assign each value to the combination object.
    • *
    • Recursively yield combinations for the next parameter using * yield*.
    • *
    • Remove the assigned value (backtrack) before trying the next * value.
    • *
    *
  4. *
* * The combination object is reused and modified during traversal, with * only copies of complete combinations being yielded. This approach * minimises memory allocation whilst maintaining correctness. * * @param index - The current parameter index being processed. * @param combination - The partial combination being built. * * Yields: Complete combinations one at a time. */ protected *_generateRecursively( index: number, combination: Record ): Generator { const log = this._log log.trace( `${CombinationsGenerator.name}.` + `_generateRecursively(${String(index)},` + `${JSON.stringify(combination)})` ) if (index === this._matrixKeys.length) { log.trace('combination complete =>', combination) yield { ...combination } return } const key = this._matrixKeys[index] const values = this._matrixValues[index] for (const value of values) { combination[key] = value yield* this._generateRecursively(index + 1, combination) // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete combination[key] } } } // ----------------------------------------------------------------------------