tiny-essentials
Version:
Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.
189 lines (188 loc) • 7.92 kB
JavaScript
/** @typedef {new (...args: any[]) => any} NewTinyClass */
/**
* @template {NewTinyClass} TBase
* @template {NewTinyClass} TExtended
* @template {NewTinyClass[]} AppliedPluginClasses
* @typedef {Object} PluginDefinition
* @property {string} name - The unique identifier for the plugin.
* @property {string[]} [dependencies] - Array of plugin names required before applying this one.
* @property {function(TBase, AppliedPluginClasses): TExtended} apply - Function that receives the base class and returns the extended class.
*/
/**
* Manages the composition of a base class with multiple optional plugins (Mixins).
* @template {NewTinyClass} T
* @template {T|NewTinyClass} oldT
*/
class TinyClassManager {
/** @typedef {[...oldT[], T]} AppliedPluginClasses */
/**
* @type {Set<string>}
* Tracks the names of successfully applied plugins to prevent duplication and check dependencies.
*/
#appliedPlugins = new Set();
/**
* Gets the list of plugins currently applied to this instance.
* @returns {string[]} Array of applied plugin names.
*/
get appliedPlugins() {
if (this.#used)
throw new Error(`[TinyClassManager] Cannot get a consumed manager instance.`);
return [...this.#appliedPlugins];
}
/**
* @type {AppliedPluginClasses}
* Tracks the classes of successfully applied plugins to prevent duplication and check dependencies.
*/
// @ts-ignore
#appliedPluginClasses = [];
/**
* Gets the list of plugin classes currently applied to this instance.
* @returns {AppliedPluginClasses} Array of applied plugin classes.
*/
get appliedPluginClasses() {
if (this.#used)
throw new Error(`[TinyClassManager] Cannot get a consumed manager instance.`);
return [...this.#appliedPluginClasses];
}
/**
* Gets the total size of the current class chain hierarchy.
* @returns {number} The count of applied plugins plus the core base.
*/
get size() {
if (this.#used)
throw new Error(`[TinyClassManager] Cannot get a consumed manager instance.`);
return this.#appliedPluginClasses.length;
}
/**
* @type {T}
* Holds the current state of the class chain.
*/
#currentClass;
/**
* Holds the current state of the class chain.
*/
get currentClass() {
if (this.#used)
throw new Error(`[TinyClassManager] Cannot get a consumed manager instance.`);
return this.#currentClass;
}
/**
* Protects the instance from being consumed or reused after transition.
*/
#used = false;
/**
* Protects the instance from being consumed or reused after transition.
*/
get used() {
return this.#used;
}
/**
* Initializes the manager with a core class.
* @param {T} coreClass - The foundational class to be extended.
* @param {oldT[]} [oldClasses=[]]
*/
constructor(coreClass,
// @ts-ignore
oldClasses = []) {
this.#currentClass = coreClass;
this.#appliedPluginClasses = [...oldClasses, coreClass];
}
/**
* Internal helper to validate dependencies, check conflicts, and transition state.
* @template {NewTinyClass} R
* @param {string} name - The plugin name.
* @param {string[]} deps - The plugin dependencies.
* @param {R} ExtendedClass - The newly extended class returned by the apply function.
* @returns {TinyClassManager<R, T | oldT>} A new manager instance.
*/
#validateAndTransition(name, deps, ExtendedClass) {
// Verifies if the current manager has already been consumed to prevent branching.
if (this.#used)
throw new Error(`[TinyClassManager] Cannot reuse a consumed manager instance.`);
// Checks if the plugin being inserted is already in the applied list.
if (this.#appliedPlugins.has(name))
throw new Error(`[TinyClassManager] Plugin conflict: "${name}" is already installed.`);
for (const dep of deps) {
if (!this.#appliedPlugins.has(dep)) {
throw new Error(`[TinyClassManager] Missing Dependency: "${name}" requires "${dep}" to be installed first.`);
}
}
const newClassManager = new TinyClassManager(ExtendedClass, this.#appliedPluginClasses);
this.#appliedPlugins.forEach((appliedName) => newClassManager.#appliedPlugins.add(appliedName));
newClassManager.#appliedPlugins.add(name);
this.#used = true;
this.#appliedPlugins.clear();
// @ts-ignore
this.#appliedPluginClasses = [];
return newClassManager;
}
/**
* Applies a plugin to the class chain using a definition object.
* @deprecated Use {@link insert} instead.
* @template {NewTinyClass} R
* @param {PluginDefinition<T, R, AppliedPluginClasses>} plugin - The plugin module to be integrated.
* @returns {TinyClassManager<R, T | oldT>} A new manager instance holding the extended class chain.
* @throws {Error} Throws if instance is already consumed, plugin is duplicate, or dependencies are missing.
*/
use(plugin) {
/**
* @type {string[]}
* Extracted dependencies array with fallback for undefined properties.
*/
const deps = plugin.dependencies || [];
/**
* @type {R}
* The new class generated by the old object-style apply wrapper.
*/
const ExtendedClass = plugin.apply(this.#currentClass, this.appliedPluginClasses);
return this.#validateAndTransition(plugin.name, deps, ExtendedClass);
}
/**
* Inserts a plugin directly by passing its apply function.
* It reads `_tinyDepName` and `_tinyDeps` statically from the returned class.
* @template {NewTinyClass} R
* @param {function(T, AppliedPluginClasses): R} applyFn - Function that receives the base class and returns the extended class.
* @returns {TinyClassManager<R, T | oldT>} A new manager instance holding the extended class chain.
* @throws {Error} Throws if static properties are missing, instance is consumed, or validation fails.
*/
insert(applyFn) {
/** * @type {R}
* The extended class returned directly from the provided function.
*/
const ExtendedClass = applyFn(this.#currentClass, this.appliedPluginClasses);
/**
* @type {string}
* The plugin name extracted from the static property.
*/
// @ts-ignore
const pluginName = ExtendedClass._tinyDepName;
if (!pluginName) {
throw new Error(`[TinyClassManager] Plugin class must define a static '_tinyDepName' property.`);
}
/**
* @type {string[]}
* The dependencies extracted from the static property, defaulting to an empty array.
*/
// @ts-ignore
const deps = ExtendedClass._tinyDeps ?? [];
if (!Array.isArray(deps) || !deps.every((name) => typeof name === 'string')) {
throw new Error(`[TinyClassManager] Plugin class must define a static array of strings in '_tinyDepName' property.`);
}
return this.#validateAndTransition(pluginName, deps, ExtendedClass);
}
/**
* Finalizes the composition and returns the fully built class.
* @returns {T} The final class representing the last extended version in the chain.
* @throws {Error} Throws if the manager instance has already been used or finalized.
*/
build() {
if (this.#used)
throw new Error(`[TinyClassManager] Cannot build from an already finalized manager.`);
this.#used = true;
this.#appliedPlugins.clear();
// @ts-ignore
this.#appliedPluginClasses = [];
return this.#currentClass;
}
}
export default TinyClassManager;