UNPKG

gnss-state

Version:

Shared singleton state service for Angular MFE architecture

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