UNPKG

gnss-state

Version:

Shared singleton state service for Angular MFE architecture

1,069 lines 76.5 kB
import { BehaviorSubject, Observable } from 'rxjs'; import { StateObjType } from '../types/state-obj.type'; import * as i0 from "@angular/core"; /** * A comprehensive reactive state management service for Angular applications that provides * centralized state storage, hierarchical organization, and robust backup/rollback functionality. * * This service implements a sophisticated state management pattern using RxJS BehaviorSubjects * to enable reactive data flow throughout Angular applications. It supports both flat and * hierarchical state organization through groups, automatic backup creation for rollback * operations, and deep merging capabilities for complex state updates. * * ## Key Features * * - **Reactive State Management**: Built on RxJS BehaviorSubjects for reactive data flow * - **Hierarchical Organization**: Support for grouped state subjects with nested structures * - **Backup & Rollback**: Automatic backup creation with reset functionality for undo operations * - **Deep Merging**: Intelligent object merging for partial state updates * - **Type Safety**: Full TypeScript support with type guards and strict typing * - **Memory Management**: Proper cleanup and resource management to prevent memory leaks * - **Change Detection**: Optimized updates with deep equality checks to prevent unnecessary emissions * * ## Usage Patterns * * ### Basic State Operations * ```typescript * // Inject the service * constructor(private stateModel: StateModel) {} * * // Set initial state * this.stateModel.set('userProfile', { id: 1, name: 'John', email: 'john@example.com' }); * * // Subscribe to state changes * this.stateModel.observe('userProfile').subscribe(profile => { * console.log('Profile updated:', profile); * }); * * // Update existing state * this.stateModel.update('userProfile', { name: 'Jane Doe' }); * * // Reset to backup value * this.stateModel.reset('userProfile'); * ``` * * ### Grouped State Management * ```typescript * // Create and populate a group * const users = [ * { id: 'user1', name: 'John', role: 'admin' }, * { id: 'user2', name: 'Jane', role: 'user' } * ]; * this.stateModel.loadGroup('users', 'id', users); * * // Access grouped subjects * this.stateModel.observe('user1', 'users').subscribe(user => { * console.log('User updated:', user); * }); * * // Update grouped state * this.stateModel.set('currentUser', { id: 3, name: 'Bob' }, 'users'); * ``` * * ### Form Integration with Backup/Reset * ```typescript * // Initialize form state * this.stateModel.set('formData', this.form.value); * * // Track form changes * this.form.valueChanges.subscribe(value => { * this.stateModel.update('formData', value); * }); * * // Reset form to initial state * onResetClick() { * this.stateModel.reset('formData'); * this.form.patchValue(this.stateModel.value('formData')); * } * ``` * * ## Architecture * * The service maintains several core registries: * - **Main Directory**: Flat registry of named BehaviorSubjects * - **Groups**: Hierarchical registry for organized state subjects * - **Backups**: Rollback data for main directory subjects * - **Group Backups**: Rollback data for grouped subjects * - **Change Tracking**: Reactive notifications for state modifications * * ## Performance Optimizations * * - Deep equality checks prevent unnecessary BehaviorSubject emissions * - Automatic cleanup prevents memory leaks * - Smart merging preserves existing state properties * - Lazy initialization of subjects and groups * * @example * ## Complete Example: User Management with Groups * ```typescript * @Component({...}) * export class UserManagementComponent implements OnInit { * constructor(private stateModel: StateModel) {} * * ngOnInit() { * // Initialize user management state * this.loadUsers(); * this.setupSubscriptions(); * } * * private loadUsers() { * const users = [ * { id: 'user1', name: 'John Doe', email: 'john@example.com', active: true }, * { id: 'user2', name: 'Jane Smith', email: 'jane@example.com', active: false } * ]; * * // Create grouped state for users * this.stateModel.loadGroup('users', 'id', users); * * // Set selected user * this.stateModel.set('selectedUser', users[0], 'users'); * } * * private setupSubscriptions() { * // React to selected user changes * this.stateModel.observe('selectedUser', 'users').subscribe(user => { * if (user) { * this.loadUserDetails(user); * } * }); * * // Monitor all group changes * this.stateModel.groupList.subscribe(groups => { * console.log('Available groups:', groups); * }); * } * * updateUser(userId: string, updates: Partial<User>) { * // Merge partial updates with existing user data * this.stateModel.dirMerge(updates, userId, 'users'); * } * * resetUserChanges(userId: string) { * // Reset user to backup state * this.stateModel.reset(userId, 'users'); * } * } * ``` * * @see {@link StateObjType} - Type definition for state values * @see {@link BehaviorSubject} - RxJS reactive primitive used for state storage * * @public * @injectable * @since 1.0.0 */ export declare class StateModel { /** * Registry of backup values for state subjects to enable rollback functionality. * * Stores the original values of state subjects when they are created or modified through the * {@link StateModel.set} method. These backup values remain immutable during subsequent * {@link StateModel.update} operations and can be restored using {@link StateModel.reset}. * This mechanism supports undo/reset patterns commonly used in form controls and dialog * components. * * @remarks * The backup system follows these rules: * - Backups are created/updated only by the `set()` method * - The `update()` method does not modify backup values * - The `reset()` method restores values from this backup registry * - Backups are stored as deep clones to prevent reference mutations * * @example * Basic backup and reset workflow: * ```typescript * // Setting creates backup automatically * stateModel.set('userForm', { name: 'John', email: 'john@example.com' }); * * // Backup contains original value * console.log(stateModel.backups['userForm']); * // Output: { name: 'John', email: 'john@example.com' } * * // Updates don't affect backup * stateModel.update('userForm', { name: 'Jane', email: 'jane@example.com' }); * * // Backup remains unchanged * console.log(stateModel.backups['userForm']); * // Output: { name: 'John', email: 'john@example.com' } * * // Reset restores from backup * stateModel.reset('userForm'); * // Current value: { name: 'John', email: 'john@example.com' } * ``` * * @see {@link StateModel.set} - Creates and updates backup values * @see {@link StateModel.update} - Modifies state without affecting backups * @see {@link StateModel.reset} - Restores state from backup values * @see {@link StateModel.groupBackups} - Backup registry for grouped subjects * * @since 1.0.0 */ backups: Record<string, StateObjType>; /** * A registry of reactive state subjects that can be observed by any component * throughout the application. This registry uses a key-value mapping pattern * where each key represents a unique state identifier and each value is a * BehaviorSubject containing the state data. * * This design pattern provides centralized state management with reactive * capabilities, allowing components to subscribe to specific state changes * without tight coupling to state producers. * * @example * ```typescript * // Subscribe to a specific state * this.stateModel.dir['userProfile'].subscribe(profile => { * console.log('User profile updated:', profile); * }); * * // Access current value * const currentUser = this.stateModel.dir['userProfile'].value; * ``` * * @see {@link StateModel.observe} For safe access to subjects with automatic creation * @see {@link StateModel.set} For creating and updating state subjects * @see {@link StateModel.update} For modifying existing state subjects * * @public * @since 1.0.0 */ dir: Record<string, BehaviorSubject<StateObjType>>; /** * A reactive registry of all group names currently managed by the state system. * * This BehaviorSubject maintains an array of string identifiers representing * all group names that exist in the {@link StateModel.groups} registry. It provides * a reactive way to track when new groups are created or existing groups are removed, * enabling components to respond to structural changes in the grouped state architecture. * * The group list is automatically maintained by the state management system: * - Group names are added when {@link StateModel.loadGroup} creates new groups * - Group names are removed when {@link StateModel.removeGroup} deletes groups * - The list is kept in sync with the actual {@link StateModel.groups} registry * * @example * Subscribe to group registry changes: * ```typescript * this.stateModel.groupList.subscribe(groupNames => { * console.log('Available groups:', groupNames); * // Output: ['users', 'settings', 'navigation'] * }); * ``` * * @example * Check if a specific group exists: * ```typescript * const hasUsersGroup = this.stateModel.groupList.value.includes('users'); * ``` * * @see {@link StateModel.groups} - The actual group registry this list tracks * @see {@link StateModel.loadGroup} - Method that adds group names to this list * @see {@link StateModel.removeGroup} - Method that removes group names from this list * * @public * @since 1.0.0 */ groupList: BehaviorSubject<string[]>; /** * Hierarchical registry of backup values for grouped state subjects to enable rollback functionality. * * This two-tier backup system maintains original values for state subjects organized within groups, * complementing the main {@link StateModel.backups} registry for ungrouped subjects. Each group * maintains its own backup registry, where backup values remain immutable during subsequent * {@link StateModel.update} operations and can be restored using {@link StateModel.reset}. * * The structure follows a group-name → subject-name → backup-value hierarchy, allowing for * fine-grained backup management within the grouped state architecture. This supports complex * UI patterns where different feature areas require independent rollback capabilities. * * @remarks * The hierarchical backup system follows these rules: * - Group backups are created/updated only by the `set()` method when a group name is specified * - The `update()` method does not modify backup values in any group * - The `reset()` method can restore individual subjects within specific groups * - Group removal via `removeGroup()` clears all backup data for that group * - Backups are stored as deep clones to prevent reference mutations * * @example * Grouped backup and reset workflow: * ```typescript * // Setting with group creates backup automatically * stateModel.set('currentUser', { id: 1, name: 'John' }, 'users'); * stateModel.set('permissions', ['read', 'write'], 'users'); * * // Group backup structure: * // { * // 'users': { * // 'currentUser': { id: 1, name: 'John' }, * // 'permissions': ['read', 'write'] * // } * // } * * // Updates don't affect group backups * stateModel.update('currentUser', { id: 1, name: 'Jane' }, 'users'); * * // Group backup remains unchanged * console.log(stateModel.groupBackups['users']['currentUser']); * // Output: { id: 1, name: 'John' } * * // Reset specific subject within group * stateModel.reset('currentUser', 'users'); * // Current value restored: { id: 1, name: 'John' } * ``` * * @see {@link StateModel.backups} - Main backup registry for ungrouped subjects * @see {@link StateModel.groups} - The grouped subjects this backup system supports * @see {@link StateModel.set} - Creates and updates group backup values * @see {@link StateModel.reset} - Restores state from group backup values * @see {@link StateModel.removeGroup} - Clears all backups for a specific group * * @public * @since 1.0.0 */ groupBackups: Record<string, Record<string, StateObjType>>; /** * A Directory of grouped BehaviorSubjects where each group is itself a BehaviorSubject * containing a Record of named BehaviorSubjects. This nested structure allows for * reactive group management where the entire group can be observed for changes, * and individual subjects within each group can also be observed independently. * * The outer Record maps group names to BehaviorSubjects, where each BehaviorSubject * contains a Record mapping subject names to their respective BehaviorSubjects. * This enables hierarchical state management with both group-level and item-level * reactivity. * * @example * ```typescript * // Access a specific subject within a group * const userSubject = this.groups['users'].value['john']; * * // Subscribe to changes in the entire group * this.groups['users'].subscribe(groupRecord => { * console.log('Group changed:', Object.keys(groupRecord)); * }); * * // Subscribe to changes in a specific subject within the group * this.groups['users'].value['john'].subscribe(userData => { * console.log('User data changed:', userData); * }); * ``` */ groups: Record<string, BehaviorSubject<Record<string, BehaviorSubject<StateObjType>>>>; /** * A reactive tracker that emits the name of the most recently updated state subject. * * This BehaviorSubject provides a centralized way to monitor state change activity * across the entire state management system. It emits the subject name whenever * any state modification occurs through the {@link StateModel.set}, {@link StateModel.update}, * or {@link StateModel.initiate} methods, enabling reactive side effects and UI updates * based on specific state changes. * * The tracker operates across both individual subjects in {@link StateModel.dir} * and grouped subjects in {@link StateModel.groups}, providing unified change * detection regardless of state organization. * * @example * Monitor all state changes: * ```typescript * this.stateModel.updated.subscribe(subjectName => { * if (subjectName) { * console.log(`State subject '${subjectName}' was updated`); * // Trigger side effects, analytics, or UI updates * } * }); * ``` * * @example * React to specific subject changes: * ```typescript * this.stateModel.updated.pipe( * filter(name => name === 'userProfile'), * switchMap(() => this.stateModel.observe('userProfile')) * ).subscribe(profile => { * // Handle user profile changes * }); * ``` * * @remarks * - Emits `undefined` on initialization * - Always emits the subject name, not the group name for grouped subjects * - Useful for implementing global state change listeners and audit trails * - Can be used to invalidate caches or trigger data synchronization * * @see {@link StateModel.set} - Method that triggers updates when setting state * @see {@link StateModel.update} - Method that triggers updates when modifying state * @see {@link StateModel.initiate} - Method that triggers updates when initializing state * * @public * @since 1.0.0 */ updated: BehaviorSubject<string | undefined>; /** * Type guard that determines whether a value is a plain object suitable for deep merging operations. * * This utility method implements a type-safe check to identify plain objects created by object * literals or the Object constructor, distinguishing them from arrays, dates, class instances, * and other specialized object types. It uses the reliable `Object.prototype.toString.call()` * approach to ensure accurate type detection across different JavaScript contexts and * inheritance chains. * * Plain objects are defined as objects with `[object Object]` as their internal [[Class]] slot, * which includes: * - Object literals: `{ key: 'value' }` * - Objects created with `new Object()` * - Objects created with `Object.create(Object.prototype)` * * Non-plain objects that return `false` include: * - Arrays: `[1, 2, 3]` * - Dates: `new Date()` * - RegExp: `/pattern/` * - Functions: `() => {}` * - Class instances: `new MyClass()` * - Built-in objects: `Math`, `JSON`, etc. * - Primitives: `null`, `undefined`, strings, numbers, booleans * * @param val - The value to test for plain object status. Accepts any type including * primitives, objects, arrays, null, or undefined. * * @returns Type predicate indicating whether the value is a plain object. When `true`, * TypeScript narrows the type to `Record<string, unknown>`, enabling safe property access * and iteration. When `false`, the value is not suitable for deep merging operations. * * @example * Basic plain object detection: * ```typescript * this.isPlainObject({}); // true * this.isPlainObject({ name: 'John' }); // true * this.isPlainObject(new Object()); // true * this.isPlainObject([1, 2, 3]); // false * this.isPlainObject(new Date()); // false * this.isPlainObject(null); // false * this.isPlainObject('string'); // false * ``` * * @example * Usage in merge operations: * ```typescript * if (this.isPlainObject(sourceObj) && this.isPlainObject(targetObj)) { * // Safe to perform recursive merge * return this.merge(sourceObj, targetObj); * } else { * // Replace non-plain objects completely * return sourceObj; * } * ``` * * @example * Type narrowing with type guard: * ```typescript * function processValue(input: unknown) { * if (this.isPlainObject(input)) { * // TypeScript knows 'input' is Record<string, unknown> * console.log(Object.keys(input)); // No type error * return input.someProperty; // Safe property access * } * // Handle non-plain objects differently * return input; * } * ``` * * @remarks * - Uses `Object.prototype.toString.call()` for reliable cross-frame type detection * - Serves as a TypeScript type guard for enhanced type safety in merge operations * - Essential for preventing incorrect recursive merging of arrays, dates, and class instances * - Performance is O(1) with minimal overhead for type checking * - Does not check for object enumerability or property descriptors * * @see {@link StateModel.merge} - Primary consumer of this type guard for safe object merging * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString * * @internal * @since 1.0.0 */ private isPlainObject; /** * Retrieves the current values from all BehaviorSubjects in the state registry. * * Extracts and returns the current values from all registered state subjects, * either from the main {@link StateModel.dir} registry or from a specific group * within {@link StateModel.groups}. The returned object uses subject names as keys * and their current values as the corresponding values, providing a snapshot * of the current state at the time of invocation. * * Values are deep cloned through the {@link StateModel.value} method to prevent * external mutations from affecting the state management system. * * @param groupName - Optional group name to limit extraction to subjects within * a specific group. When `null` (default), retrieves values from all subjects * in the main directory. When specified, only retrieves values from subjects * within the named group. * * @returns A record object where keys are subject names and values are the * current state values of the corresponding BehaviorSubjects. Returns an * empty object if no subjects exist or if the specified group doesn't exist. * * @example * Get all values from the main directory: * ```typescript * const allValues = this.stateModel.getDirValues(); * // Returns: { userProfile: {...}, settings: {...}, navigation: {...} } * ``` * * @example * Get values from a specific group: * ```typescript * const userValues = this.stateModel.getDirValues('users'); * // Returns: { currentUser: {...}, permissions: [...] } * ``` * * @see {@link StateModel.dir} - Main state subject registry * @see {@link StateModel.groups} - Grouped state subject registry * @see {@link StateModel.value} - Method used internally to extract values * * @public * @since 1.0.0 */ getDirValues(groupName?: string | null): Record<string, StateObjType>; /** * Determines whether a BehaviorSubject with the specified name exists in the state registry. * * This method performs existence checks in either the main {@link StateModel.dir} registry * or within a specific group in {@link StateModel.groups}. When a group name is provided, * the method first verifies the group exists before checking for the subject within that group. * This dual-level checking supports the hierarchical state management architecture. * * @param subjectName - The name of the BehaviorSubject to check for existence. * This parameter is required and represents the unique identifier for the state subject. * @param groupName - Optional group name to limit the search to subjects within * a specific group. When `null` (default), searches in the main directory registry. * When specified, searches only within the named group. * * @returns `true` if the subject exists in the specified location (main directory * or within the specified group), `false` otherwise. Returns `false` if the * specified group doesn't exist. * * @example * Check for subject in main directory: * ```typescript * const hasUserProfile = this.stateModel.has('userProfile'); * // Returns true if 'userProfile' exists in main directory * ``` * * @example * Check for subject within a specific group: * ```typescript * const hasCurrentUser = this.stateModel.has('currentUser', 'users'); * // Returns true if 'currentUser' exists within 'users' group * ``` * * @see {@link StateModel.dir} - Main state subject registry * @see {@link StateModel.groups} - Grouped state subject registry * @see {@link StateModel.observe} - Method for safely accessing subjects with automatic creation * * @public * @since 1.0.0 */ has(subjectName: string, groupName?: string | null): boolean; /** * Initializes or resets a BehaviorSubject to an undefined state within the state management system. * * This method creates new BehaviorSubjects with an initial value of `undefined`, or resets existing * subjects to `undefined` if they currently contain other values. It supports both standalone subjects * in the main {@link StateModel.dir} registry and grouped subjects within {@link StateModel.groups}. * When working with grouped subjects, the method will automatically create the parent group if it * doesn't exist. * * The initialization process includes: * - Creating new BehaviorSubjects with `undefined` as the initial value * - Resetting existing subjects to `undefined` (no-op if already `undefined`) * - Setting up corresponding backup entries for rollback functionality * - Auto-creating group structures when specified * - Triggering the {@link StateModel.updated} notification system * * @param subjectName - The unique identifier for the BehaviorSubject to initialize or reset. * Must be a non-empty string that will serve as the key in the state registry. * @param groupName - Optional group name to organize the subject within a hierarchical structure. * When `null` (default), the subject is created in the main directory. When specified, the subject * is created within the named group, and the group itself is created if it doesn't exist. * * @returns void - This method performs side effects but returns no value. * * @example * Initialize a standalone subject: * ```typescript * // Creates a new BehaviorSubject with undefined value * this.stateModel.initiate('userProfile'); * * // Access the initialized subject * this.stateModel.observe('userProfile').subscribe(value => { * console.log(value); // undefined initially * }); * ``` * * @example * Initialize a subject within a group: * ```typescript * // Creates both 'users' group and 'currentUser' subject if they don't exist * this.stateModel.initiate('currentUser', 'users'); * * // Reset an existing grouped subject to undefined * this.stateModel.set('currentUser', { id: 1, name: 'John' }, 'users'); * this.stateModel.initiate('currentUser', 'users'); // Resets to undefined * ``` * * @example * Resetting existing subjects: * ```typescript * // Subject with existing data * this.stateModel.set('settings', { theme: 'dark', lang: 'en' }); * * // Reset to undefined (triggers backup and notification) * this.stateModel.initiate('settings'); * * // No-op if already undefined * this.stateModel.initiate('settings'); // No change, no notification * ``` * * @remarks * - Early return optimization: If the target subject already has an `undefined` value, the method returns early without triggering notifications * - Group auto-creation: When a group name is specified but the group doesn't exist, both the group and backup registry are automatically created * - Backup management: All initiated subjects receive corresponding backup entries set to `undefined` * - Notification system: The {@link StateModel.updated} BehaviorSubject is notified with the subject name after successful operations * * @see {@link StateModel.set} - For setting subjects with specific initial values * @see {@link StateModel.observe} - For safely accessing and subscribing to subjects * @see {@link StateModel.reset} - For restoring subjects from backup values * @see {@link StateModel.loadGroup} - For bulk group creation with multiple subjects * * @public * @since 1.0.0 */ initiate(subjectName: string, groupName?: string | null): void; /** * Performs deep equality comparison between two values to determine if they are structurally identical. * * This utility method implements recursive deep comparison for complex data structures including * objects, arrays, and primitives. It's used throughout the state management system to prevent * unnecessary BehaviorSubject emissions when values haven't actually changed, optimizing * performance and preventing infinite subscription loops. * * The comparison follows these rules: * - Uses strict equality (`===`) for primitives and identical references * - Considers `null` and `undefined` as distinct values (not equal to each other) * - Recursively compares array elements in order * - Recursively compares object properties by key-value pairs * - Returns `false` for mismatched types or structures * * @param value - The first value to compare. Can be any type including primitives, * objects, arrays, null, or undefined. * @param other - The second value to compare against. Can be any type including * primitives, objects, arrays, null, or undefined. * * @returns `true` if the values are deeply equal, `false` if they differ in structure * or content, or `undefined` if the comparison cannot be determined (currently this * method always returns a boolean). * * @example * Primitive comparisons: * ```typescript * this.stateModel.isEqual(5, 5); // true * this.stateModel.isEqual('hello', 'hi'); // false * this.stateModel.isEqual(null, undefined); // false * ``` * * @example * Array comparisons: * ```typescript * this.stateModel.isEqual([1, 2, 3], [1, 2, 3]); // true * this.stateModel.isEqual([1, 2], [1, 2, 3]); // false * this.stateModel.isEqual([[1, 2]], [[1, 2]]); // true (nested arrays) * ``` * * @example * Object comparisons: * ```typescript * const obj1 = { name: 'John', age: 30, hobbies: ['read', 'code'] }; * const obj2 = { name: 'John', age: 30, hobbies: ['read', 'code'] }; * const obj3 = { age: 30, name: 'John', hobbies: ['read', 'code'] }; * * this.stateModel.isEqual(obj1, obj2); // true * this.stateModel.isEqual(obj1, obj3); // true (key order doesn't matter) * ``` * * @example * Usage in state management optimization: * ```typescript * // Prevents unnecessary emissions * const currentValue = this.dir['userProfile'].value; * if (!this.isEqual(currentValue, newValue)) { * this.dir['userProfile'].next(newValue); * } * ``` * * @remarks * - This method is used internally by {@link StateModel.set}, {@link StateModel.update}, * and {@link StateModel.reset} to optimize performance * - Handles circular references gracefully by comparing object structure * - Performance scales with object depth and complexity * - Does not compare object prototypes or non-enumerable properties * * @see {@link StateModel.set} - Uses this method to prevent duplicate state updates * @see {@link StateModel.update} - Uses this method for change detection * @see {@link StateModel.dirEquals} - Higher-level equality check for directory objects * * @public * @since 1.0.0 */ isEqual(value: unknown, other: unknown): boolean | undefined; /** * Creates and initializes a hierarchical group within the state management system. * * This method establishes a new group in the {@link StateModel.groups} registry as a * BehaviorSubject containing a Record of named BehaviorSubjects. The group provides * hierarchical state organization where both the group container and individual subjects * within it are reactive. Optionally populates the group with BehaviorSubjects created * from an array of objects, using a specified property as the subject identifier. * * The method performs the following operations: * - Creates the group BehaviorSubject if it doesn't exist * - Optionally creates individual BehaviorSubjects from object array using keyName as identifier * - Updates the reactive {@link StateModel.groupList} registry * - Maintains referential integrity within the hierarchical state structure * * @param groupName - The unique identifier for the group to create or initialize. * This name will be used as the key in the {@link StateModel.groups} registry. * @param keyName - Optional property name within objects to use as BehaviorSubject * identifiers. When provided with `objArray`, this property's value becomes the * subject name within the group. Must exist as a string or number property in each object. * @param objArray - Optional array of objects to transform into BehaviorSubjects within * the group. Each object becomes the initial value of a BehaviorSubject, with the * subject name derived from the object's `keyName` property. * * @example * Create an empty group: * ```typescript * this.stateModel.loadGroup('users'); * // Creates: groups['users'] = BehaviorSubject<Record<string, BehaviorSubject<StateObjType>>>({}) * ``` * * @example * Populate group from object array: * ```typescript * const userData = [ * { id: 'user1', name: 'John', role: 'admin' }, * { id: 'user2', name: 'Jane', role: 'user' } * ]; * this.stateModel.loadGroup('users', 'id', userData); * // Creates subjects: groups['users'].value['user1'], groups['users'].value['user2'] * ``` * * @see {@link StateModel.groups} - The hierarchical group registry this method modifies * @see {@link StateModel.groupList} - Reactive list of all group names, updated by this method * @see {@link StateModel.removeGroup} - Method for removing entire groups * @see {@link StateModel.observe} - Method for accessing subjects within groups * * @public * @since 1.0.0 */ loadGroup(groupName: string, keyName?: string | null, objArray?: unknown[] | null): void; /** * Performs a deep merge of two objects, combining their properties into a new object. * * This method recursively merges nested objects while preserving the structure of both input objects. * Properties from `newObj` take precedence over properties from `oldObj`. For nested plain objects, * the merge operation continues recursively. For primitives, arrays, dates, and other non-plain * object types, values from `newObj` completely replace corresponding values from `oldObj`. * * The merge operation follows these rules: * - If `newObj` is not a plain object (null, undefined, array, etc.), returns `oldObj` unchanged * - If `oldObj` is null or undefined, treats it as an empty object for merging purposes * - Nested plain objects are merged recursively using the same rules * - Non-object values (primitives, arrays, dates, etc.) from `newObj` replace values in `oldObj` * - Returns a new object without mutating the original input objects * * @param newObj - The source object whose properties will be merged into the base object. * If not a plain object, the merge operation returns `oldObj` unchanged. * @param oldObj - The base object to merge into. Null or undefined values are treated as empty objects. * * @returns A new merged object containing properties from both inputs, with `newObj` properties * taking precedence. Returns `oldObj` unchanged if `newObj` is not a plain object. * * @example * Basic object merging: * ```typescript * const base = { name: 'John', age: 30, city: 'New York' }; * const updates = { age: 31, country: 'USA' }; * const result = this.stateModel.merge(updates, base); * // Result: { name: 'John', age: 31, city: 'New York', country: 'USA' } * ``` * * @example * Nested object merging: * ```typescript * const base = { user: { name: 'John', settings: { theme: 'light', lang: 'en' } } }; * const updates = { user: { settings: { theme: 'dark' } } }; * const result = this.stateModel.merge(updates, base); * // Result: { user: { name: 'John', settings: { theme: 'dark', lang: 'en' } } } * ``` * * @example * Handling non-plain objects: * ```typescript * const base = { items: [1, 2, 3], date: new Date('2023-01-01') }; * const updates = { items: [4, 5], date: new Date('2024-01-01') }; * const result = this.stateModel.merge(updates, base); * // Arrays and dates are replaced, not merged * // Result: { items: [4, 5], date: new Date('2024-01-01') } * ``` * * @example * State update pattern: * ```typescript * // Merge partial user profile updates * const currentProfile = this.stateModel.value('userProfile'); * const profileUpdates = { preferences: { notifications: false } }; * const mergedProfile = this.stateModel.merge(profileUpdates, currentProfile); * this.stateModel.set('userProfile', mergedProfile); * ``` * * @remarks * - Uses {@link StateModel.isPlainObject} to determine if objects can be merged recursively * - Preserves prototype chains and does not modify input objects * - Performance scales with object depth and number of properties * - Ideal for implementing partial state updates and configuration merging * * @see {@link StateModel.isPlainObject} - Helper method used to identify mergeable objects * @see {@link StateModel.update} - Method that could benefit from merge operations * @see {@link StateModel.set} - Method for applying merged results to state * * @public * @since 1.0.0 */ merge(newObj: Record<string, unknown> | null | undefined, oldObj: Record<string, unknown> | null | undefined): Record<string, unknown> | null | undefined; /** * Compares an external object with the current value of a state subject for * deep equality. * * This method performs deep equality comparison between a provided object and the current * value of a BehaviorSubject stored in either the main {@link StateModel.dir} registry * or within a specific group in {@link StateModel.groups}. It leverages the internal * {@link StateModel.isEqual} method to provide structural comparison rather than reference * equality, making it ideal for change detection and validation scenarios. * * The comparison process follows this hierarchy: * 1. First checks for the subject in the main directory registry * 2. If not found and a group name is provided, searches within the specified group * 3. Returns `undefined` if the subject doesn't exist in either location * 4. Performs deep equality comparison if the subject is found * * @param obj - The external object to compare against the state subject's current value. * Can be any type including primitives, objects, arrays, null, or undefined. * @param dirObjName - The name of the state subject to compare against. This identifier * is used to locate the BehaviorSubject in either the main directory or specified group. * @param groupName - Optional group name to limit the search to subjects within a specific * group. When `null` (default), searches only in the main directory. When specified, * searches only within the named group if not found in the main directory. * * @returns `true` if the objects are deeply equal, `false` if they differ in structure * or content, or `undefined` if the specified subject doesn't exist in the target location. * * @example * Compare with main directory subject: * ```typescript * const userProfile = { id: 1, name: 'John', email: 'john@example.com' }; * const isEqual = this.stateModel.dirEquals(userProfile, 'currentUser'); * * if (isEqual === true) { * console.log('Objects are identical'); * } else if (isEqual === false) { * console.log('Objects differ'); * } else { * console.log('Subject does not exist'); * } * ``` * * @example * Compare with grouped subject: * ```typescript * const settingsData = { theme: 'dark', language: 'en', notifications: true }; * const isEqual = this.stateModel.dirEquals(settingsData, 'preferences', 'user'); * * // Use result for change detection * if (isEqual === false) { * // Settings have changed, update UI or trigger side effects * this.stateModel.set('preferences', settingsData, 'user'); * } * ``` * * @example * Form validation use case: * ```typescript * // Check if form data matches current state before submission * const formData = this.userForm.value; * const hasChanges = this.stateModel.dirEquals(formData, 'userProfile') === false; * * if (hasChanges) { * // Enable save button or show unsaved changes indicator * this.showUnsavedChanges = true; * } * ``` * * @remarks * - This method is particularly useful for change detection in reactive forms and components * - The comparison is structural, not referential, allowing for meaningful equality checks * - Returning `undefined` allows callers to distinguish between "different values" and "subject not found" * - Performance scales with object complexity due to deep comparison algorithm * * @see {@link StateModel.isEqual} - The underlying deep equality comparison method * @see {@link StateModel.dir} - Main state subject registry searched by this method * @see {@link StateModel.groups} - Grouped state registry searched when group name is specified * @see {@link StateModel.has} - Method to check subject existence without value comparison * * @public * @since 1.0.0 */ dirEquals(obj: unknown, dirObjName: string, groupName?: string | null): boolean | undefined; /** * Merges a new object with the current value of a state subject, updating the subject if changes are detected. * * This method provides intelligent merging capabilities by combining a new object with the existing * value of a BehaviorSubject stored in either the main {@link StateModel.dir} registry or within * a specific group in {@link StateModel.groups}. It leverages the internal {@link StateModel.merge} * method for deep merging and automatically updates the state subject only when the merged result * differs from the current value, providing optimization for unnecessary operations. * * The merge operation follows this workflow: * 1. Locates the target state subject in the main directory or specified group * 2. If the subject doesn't exist, creates it using {@link StateModel.set} with the new object * 3. If the subject exists, performs a deep merge of the new object with the current value * 4. Updates the subject only if the merged result differs from the current value * 5. Returns the final merged result for further use * * This method is ideal for implementing partial state updates, configuration merging, and * incremental data modifications without losing existing state properties. * * @param newObj - The source object to merge with the current state subject value. * Properties from this object take precedence during the merge operation. If not a plain * object (null, undefined, array, etc.), the merge may not behave as expected. * @param dirObjName - The name of the state subject to merge with. This identifier is used * to locate the BehaviorSubject in either the main directory or specified group. * @param groupName - Optional group name to locate the subject within a hierarchical structure. * When `null` (default), searches in the main directory. When specified, searches within * the named group and creates the subject there if it doesn't exist. * * @returns The merged object result, or the original `newObj` if the target subject didn't * exist and was newly created. Returns `undefined` if the merge operation cannot be completed. * * @example * Merge with existing main directory subject: * ```typescript * // Existing state * this.stateModel.set('userProfile', { name: 'John', email: 'john@example.com', age: 30 }); * * // Merge partial updates * const updates = { email: 'john.doe@example.com', city: 'New York' }; * const result = this.stateModel.dirMerge(updates, 'userProfile'); * // Result: { name: 'John', email: 'john.doe@example.com', age: 30, city: 'New York' } * ``` * * @example * Merge with grouped subject: * ```typescript * // Existing grouped state * this.stateModel.set('preferences', { theme: 'light', language: 'en' }, 'settings'); * * // Merge new preferences * const newPrefs = { theme: 'dark', notifications: true }; * const result = this.stateModel.dirMerge(newPrefs, 'preferences', 'settings'); * // Result: { theme: 'dark', language: 'en', notifications: true } * ``` * * @example * Auto-creation when subject doesn't exist: * ```typescript * // Subject doesn't exist yet * const initialData = { status: 'active', priority: 'high' }; * const result = this.stateModel.dirMerge(initialData, 'taskState'); * // Creates new subject with initialData and returns it * ``` * * @example * Nested object merging: * ```typescript * // Existing nested state * this.stateModel.set('appConfig', { * ui: { theme: 'light', sidebar: 'expanded' }, * api: { timeout: 5000, retries: 3 } * }); * * // Merge partial nested updates * const updates = { ui: { theme: 'dark' }, api: { timeout: 8000 } }; * const result = this.stateModel.dirMerge(updates, 'appConfig'); * // Result: { * // ui: { theme: 'dark', sidebar: 'expanded' }, * // api: { timeout: 8000, retries: 3 } * // } * ``` * * @remarks * - Uses {@link StateModel.merge} internally for deep merging operations * - Automatically creates missing subjects using {@link StateModel.set} when needed * - Updates subjects only when merged results differ from current values (performance optimization) * - Supports both standalone subjects and grouped subjects within hierarchical organization * - The merge operation preserves existing properties not present in the new object * - Does not affect backup values - only updates current state values * * @see {@link StateModel.merge} - The underlying deep merge method used by this operation * @see {@link StateModel.set} - Method used to create subjects when they don't exist * @see {@link StateModel.isEqual} - Method used for change detection optimization * @see {@link StateModel.dir} - Main state subject registry * @see {@link StateModel.groups} - Grouped state registry for hierarchical organization * @see {@link StateModel.dirEquals} - Method for comparing objects with state subjects * * @public * @since 1.0.0 */ dirMerge(newObj: Record<string, unknown> | null | undefined, dirObjName: string, groupName?: string | null): Record<string, unknown> | null | undefined; /** * Provides safe access to a BehaviorSubject with automatic initialization if it doesn't exist. * * This method serves as the primary access point for retrieving BehaviorSubjects from either * the main {@link StateModel.dir} registry or from grouped subjects within {@link StateModel.groups}. * It implements a fail-safe pattern by automatically creating missing subjects or groups with * `undefined` initial values, enabling reactive subscription patterns even when the target * state hasn't been explicitly initialized yet. * * The method follows a hierarchical search pattern: * - For ungrouped subjects: searches in the main directory registry * - For grouped subjects: searches within the specified group, creating the group if necessary * - Auto-initializes missing subjects using {@link StateModel.initiate} with `undefined` values * - Guarantees that a valid BehaviorSubject is always returned for subscription * * This design pattern supports reactive programming where components can subscribe to state * changes before the state producer has set initial values, preventing timing-related issues * in component initialization and data flow. * * @param subjectName - The unique identifier for the BehaviorSubject to retrieve or create. * Must be a non-empty string that serves as the key in the state registry. * @param groupName - Optional group name to locate the subject within a hierarchical structure. * When `null` (default), searches in the main directory. When specified, searches within * the named group and creates the group if it doesn't exist. * * @returns A BehaviorSubject that can be immediately subscribed to. If the subject didn't * exist, it will be initialized with an `undefined` value and can be updated later through * {@link StateModel.set} or {@link StateModel.update} methods. * * @example * Subscribe to an ungrouped subject: * ```typescript * // Safe subscription even if 'userProfile' doesn't exist yet * this.stateModel.observe('userProfile').subscribe(profile => { * if (profile) { * console.log('User profile loaded:', profile); * } else { * console.log('User profile not yet initialized'); * } * }); * ``` * * @exa