xpm
Version:
The xPack project manager command line tool
332 lines (301 loc) • 11 kB
text/typescript
/*
* 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<string, string>
// ============================================================================
/**
* 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:
*
* <ol>
* <li>Takes arrays of parameter names (keys) and their corresponding value
* arrays.</li>
* <li>Recursively iterates through each parameter, selecting one value at a
* time.</li>
* <li>Yields complete combinations one at a time without storing them all
* in memory.</li>
* <li>Backtracks to explore other value combinations.</li>
* </ol>
*
* 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
* <code>matrixKeys</code>, where <code>matrixValues[i]</code> contains
* all possible values for the parameter <code>matrixKeys[i]</code>.
*
* 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:
*
* <ul>
* <li>For a matrix with 10 parameters and 5 values each (9,765,625
* combinations), only one combination object exists in memory at a
* time, regardless of matrix size.</li>
* <li>No array allocation or storage required.</li>
* <li>Immediate processing of each combination without waiting for all to
* be generated.</li>
* </ul>
*
* Example usage:
* ```typescript
* for (const combo of generator.generate()) {
* // Process one combination at a time
* await processConfiguration(combo)
* }
* ```
*
* <b>Yields:</b> Individual matrix combinations one at a time.
*/
*generate(): Generator<MatrixCombination> {
// 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:
*
* <ol>
* <li><b>Base case:</b> If all parameters have been assigned values
* (<code>index === matrixKeys.length</code>), yield a copy of the
* current combination and return.</li>
* <li><b>Recursive case:</b> For the parameter at the current index:
* <ul>
* <li>Iterate through all possible values for this parameter.</li>
* <li>Assign each value to the combination object.</li>
* <li>Recursively yield combinations for the next parameter using
* <code>yield*</code>.</li>
* <li>Remove the assigned value (backtrack) before trying the next
* value.</li>
* </ul>
* </li>
* </ol>
*
* 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.
*
* <b>Yields:</b> Complete combinations one at a time.
*/
protected *_generateRecursively(
index: number,
combination: Record<string, string>
): Generator<MatrixCombination> {
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]
}
}
}
// ----------------------------------------------------------------------------