/******************************************************************************************************* * MJ Global Class Factory handles both the registration and instantiation of any class that we need to create across any MJ Project * * The idea is to have a global place where we can register a subclass for a given base class and then call a simple class factory method to * instantiate whatever class we need. This allows any module at any time to register their new class for a given base class as a sub-class * and we will dynamically instantiate that sub-class from that point forward ******************************************************************************************************/ /** * Data structure to track the class registrations */ export declare class ClassRegistration { BaseClass: any; SubClass: any; RootClass: any; Key: string | null; Priority: number; /** * Optional structured metadata. Useful when callers want to attach * filterable/sortable attributes to a registration without polluting the * Key string (e.g. form-panel slots: { entity, slot, sortKey }). * * Pair with `ClassFactory.GetAllRegistrationsByMetadata()` / * `GetAllRegistrationsByKeyPrefix()` / `GetAllRegistrationsByKeyPattern()` * to discover registrations beyond exact-key matching. */ Metadata?: Record; } /** * The outcome of a {@link ClassFactory.TryCreateInstance} call — an EXPLICIT resolution result * so callers can distinguish "a registered subclass was found and instantiated" from "no * registration matched the key and we fell back to the anchor base class". * * ## Why this type exists * {@link ClassFactory.CreateInstance} has NEVER returned `null` for an unregistered key — it * falls back to `new BaseClass(...)`. Call sites written as `const x = CreateInstance(Base, key); * if (x) { use it } else { error }` therefore have a DEAD else-branch and silently install a * hollow base-class object. That failure mode is invisible until something calls a method the * base does not implement. `TryCreateInstance` makes the distinction explicit and checkable. */ export type ClassResolutionResult = { /** * `true` only when a REGISTERED subclass matched the requested key. `false` means the key did * not resolve — check {@link Instance} to see whether a base-class fallback was produced. */ Resolved: boolean; /** * The instance to use, or `null`. * * - `Resolved: true` → the registered subclass instance. * - `Resolved: false` and the anchor base is marked `@RequiresSubclass()` → `null` * (the base cannot function standalone, so no fallback is produced). * - `Resolved: false` and no marker → the base-class fallback instance. This is a legitimate, * long-standing pattern (e.g. `BaseEntity`, which is fully functional standalone). */ Instance: T | null; /** Human-readable explanation, present whenever `Resolved` is `false`. */ Reason?: string; }; /** * Shape of a base class that opts in to the "I cannot be instantiated standalone" contract. * * TypeScript's `abstract` keyword is ERASED at runtime — there is no marker property, and plain * JS will happily `new` an abstract class — so abstractness cannot be introspected. Bases that * genuinely cannot function without a subclass therefore declare this static marker explicitly. */ /** * ClassFactory is used to register and create instances of classes. It is a singleton class that can be used to register a sub-class for a given base class and key. Do NOT directly attempt to instantiate this class, * instead use the static Instance property of the MJGlobal class to get the instance of the ClassFactory for your application. */ export declare class ClassFactory { private _registrations; /** Per-base fallback-warning counter, backing the volume cap in reportResolutionFailure. */ private _fallbackCountByBase; /** Fallback warnings emitted per base class before summarising and going quiet. */ private static readonly MAX_FALLBACK_REPORTS_PER_BASE; /** * Memoized results of {@link GetRegistration}, keyed by `baseClassName|normalizedKey`. * GetRegistration is on extremely hot paths (every `CreateInstance`, including one call * per entity field during hydration) and otherwise re-`filter()`s the entire global * registration list on every call. The map is fully cleared whenever a new registration * is added (see {@link Register}) so it can never serve a stale result — registrations are * almost always all added at startup, so in practice the cache is built once and reused. * A `null` value is a cached "no registration found" (still a valid, useful memo). */ private _registrationCache; /** * Registered lazy loader callbacks. When `GetRegistrationAsync` or `CreateInstanceAsync` * cannot find a registration synchronously, these loaders are called in order until one * succeeds (returns `true`). This allows multiple consumers/layers to register their own * lazy loading strategies (e.g., Angular chunk loading, server-side dynamic imports). * * Each loader receives the base class name and key, and should return `true` if it * successfully loaded the module containing the requested class registration. */ private _lazyLoaders; /** * `baseClassName|normalizedKey` pairs whose base-class-fallback diagnostic has already been * emitted, so a hot-path resolution failure logs once instead of on every call. */ private _reportedResolutionFailures; /** * Registers a lazy loader callback that will be called when a class registration cannot * be found synchronously. Multiple loaders can be registered and will be called in order * until one succeeds. * * @param loader A function that receives (baseClassName, key) and returns a Promise * indicating whether it successfully loaded the module containing the registration. */ RegisterLazyLoader(loader: (baseClassName: string, key: string) => Promise): void; /** * Attempts to lazy-load a missing registration by calling registered lazy loaders in order. * Returns true if any loader successfully loaded the requested class. */ private tryLazyLoad; /** * Async version of GetRegistration that supports lazy loading. If no registration is found * synchronously and lazy loaders are registered, attempts to load the missing module before * retrying the lookup. * * @param baseClass The base class to look up * @param key Optional key to differentiate registrations * @returns The matching ClassRegistration, or null if not found even after lazy loading */ GetRegistrationAsync(baseClass: unknown, key?: string | null): Promise; /** * Async version of CreateInstance that supports lazy loading. If no registration is found * synchronously and lazy loaders are registered, attempts to load the missing module before * retrying and creating the instance. * * Falls back to instantiating the base class directly if no registration is found even * after lazy loading (same behavior as the sync CreateInstance) — including throwing when the * anchor base is marked `@RequiresSubclass()`. */ CreateInstanceAsync(baseClass: unknown, key?: string | null, ...params: unknown[]): Promise; /** * Explicit-result, lazy-loading-aware sibling of {@link TryCreateInstance}. Never throws for * an unresolved key. */ TryCreateInstanceAsync(baseClass: unknown, key?: string | null, ...params: unknown[]): Promise>; /** * Use this method or the @RegisterClass decorator to register a sub-class for a given base class. * @param baseClass A reference to the base class you are registering a sub-class for * @param subClass A reference to the sub-class you are registering * @param key A key can be used to differentiate registrations for the same base class/sub-class combination. For example, in the case of BaseEntity and Entity object subclasses we'll have a LOT of entries and we want to get the highest priority registered sub-class for a specific key. In that case, the key is the entity name, but the key can be any value you want to use to differentiate registrations. * @param priority Higher priority registrations will be used over lower priority registrations. If there are multiple registrations for a given base class/sub-class/key combination, the one with the highest priority will be used. If there are multiple registrations with the same priority, the last one registered will be used. Finally, if you do NOT provide this setting, the order of registrations will increment the priority automatically so dependency injection will typically care care of this. That is, in order for Class B, a subclass of Class A, to be registered properly, Class A code has to already have been loaded and therefore Class A's RegisterClass decorator was run. In that scenario, if neither Class A or B has a priority setting, Class A would be 1 and Class B would be 2 automatically. For this reason, you only need to explicitly set priority if you want to do something atypical as this mechanism normally will solve for setting the priority correctly based on the furthest descendant class that is registered. * @param skipNullKeyWarning If true, will not print a warning if the key is null or undefined. This is useful for cases where you know that the key is not needed and you don't want to see the warning in the console. * @param autoRegisterWithRootClass If true, will automatically register the subclass with the root class of the baseClass hierarchy. This ensures proper priority ordering when multiple subclasses are registered in a hierarchy. Defaults to false to preserve the original registration contract where classes are stored under the baseClass you specify. */ Register(baseClass: unknown, subClass: unknown, key?: string | null, priority?: number, skipNullKeyWarning?: boolean, autoRegisterWithRootClass?: boolean, metadata?: Record): void; /** * Creates an instance of the class registered for the given base class and key. * * If no registration is found, falls back to instantiating the base class itself — a * long-standing, deliberate behavior that legitimate consumers (notably `BaseEntity`) rely on. * **This method therefore does NOT return `null` for an unregistered key**, so `if (instance)` * is not a valid resolution-failure test. Use {@link TryCreateInstance} when you need to know * whether the key actually resolved. * * @throws when the key does not resolve AND the anchor base class declares * `@RequiresSubclass()` (i.e. it cannot function standalone). Bases without * that marker keep the historical fallback behavior and only emit a structured warning. */ CreateInstance(baseClass: unknown, key?: string | null, ...params: unknown[]): T | null; /** * Explicit-result sibling of {@link CreateInstance}. Never throws for an unresolved key — * returns a {@link ClassResolutionResult} so the caller can branch on `Resolved`. * * ```typescript * const res = MJGlobal.Instance.ClassFactory.TryCreateInstance(MyProviderBase, key); * if (!res.Resolved || !res.Instance) { * LogError(`provider '${key}' did not resolve: ${res.Reason}`); * return; // skip — do NOT install a hollow base instance * } * use(res.Instance); * ``` */ TryCreateInstance(baseClass: unknown, key?: string | null, ...params: unknown[]): ClassResolutionResult; /** * Single shared resolution path behind `CreateInstance`, `TryCreateInstance`, and * `CreateInstanceAsync` — so the sync and async surfaces (and the throwing and non-throwing * surfaces) can never drift apart in how they treat a fallback. */ private resolveAndInstantiate; /** * Builds the diagnostic message for a failed key resolution. The registered-key list is the * highest-value part: a typo'd or tree-shaken key is immediately obvious next to the keys the * factory actually knows about. */ private describeResolutionFailure; /** * Emits the fallback diagnostic exactly once per (baseClass, key) pair — resolution happens on * very hot paths (once per entity-field hydration), so an un-deduped log would be a firehose. * A captured stack is included so the offending call site is identifiable. */ private reportResolutionFailure; /** * Returns all registrations for a given base class and key. If key is not provided, will return all registrations for the base class. * @param baseClass * @param key * @returns */ GetAllRegistrations(baseClass: unknown, key?: string | null): ClassRegistration[]; /** * Returns all registrations for a given base class whose `Key` STARTS WITH the * provided prefix (case-insensitive, trimmed). Useful when registrations follow * a naming convention with a structured prefix (e.g. `":..."`). * * Prefer `GetAllRegistrationsByMetadata` when the discriminating data is * structured — putting tuples in the key string is fragile. */ GetAllRegistrationsByKeyPrefix(baseClass: unknown, keyPrefix: string): ClassRegistration[]; /** * Returns all registrations for a given base class whose `Key` matches the * provided regex (tested against the trimmed-but-original-case key). Use for * more nuanced discovery patterns than the prefix helper handles. */ GetAllRegistrationsByKeyPattern(baseClass: unknown, pattern: RegExp): ClassRegistration[]; /** * Returns all registrations for a given base class whose attached `Metadata` * bag satisfies the predicate. Registrations with no metadata are passed * `undefined` to the predicate. * * This is the recommended discovery path for structured per-registration * data (e.g. form-panel slots that filter by `{ entity, slot }`). It avoids * the brittleness of encoding tuples into the Key string. */ GetAllRegistrationsByMetadata(baseClass: unknown, predicate: (metadata: Record | undefined, registration: ClassRegistration) => boolean): ClassRegistration[]; /** * Returns the registration with the highest priority for a given base class and key. If key is not provided, will return the registration with the highest priority for the base class. */ GetRegistration(baseClass: unknown, key?: string | null): ClassRegistration | null; /** * Uncached core of {@link GetRegistration}: filters all matching registrations and returns * the highest-priority (last-registered on ties). Kept private so the public accessor can * memoize without the cache logic obscuring the resolution rule. */ private resolveRegistration; /** * Returns all registrations that have the specified root class, regardless of what base class was used in the registration. * This is useful for finding all registrations in a class hierarchy. * @param rootClass The root class to search for * @param key Optional key to filter results * @returns Array of matching registrations */ GetRegistrationsByRootClass(rootClass: unknown, key?: string | null): ClassRegistration[]; } //# sourceMappingURL=ClassFactory.d.ts.map