UNPKG

@fivexlabs/ng-terminus

Version:

A comprehensive Angular library for managing RxJS subscriptions and preventing memory leaks with advanced features

1 lines โ€ข 97.2 kB
{"version":3,"file":"fivexlabs-ng-terminus.mjs","sources":["../../src/lib/operators/take-until-destroyed.ts","../../src/lib/services/subscription-manager.service.ts","../../src/lib/utils/subscription-utils.ts","../../src/lib/services/subscription-debugger.service.ts","../../src/lib/operators/http-operators.ts","../../src/lib/operators/forms-operators.ts","../../src/lib/ng-terminus.module.ts","../../src/lib/operators/take-until-route.ts","../../src/lib/operators/visibility-operators.ts","../../src/lib/testing/subscription-testing.ts","../../src/lib/utils/memory-optimization.ts","../../src/public-api.ts","../../src/fivexlabs-ng-terminus.ts"],"sourcesContent":["import { DestroyRef, inject } from '@angular/core';\nimport { Observable, takeUntil, Subject } from 'rxjs';\n\n/**\n * An RxJS operator that automatically completes the source observable\n * when the associated Angular component, directive, or service is destroyed.\n * \n * This operator prevents memory leaks by tying the observable lifecycle\n * to Angular's component lifecycle through DestroyRef.\n * \n * @param destroyRef Optional DestroyRef instance. If not provided, \n * it will be automatically injected using Angular's inject() function.\n * @returns An operator function that can be used in a pipe() chain\n * \n * @example\n * ```typescript\n * // Basic usage with automatic DestroyRef injection\n * constructor(private dataService: DataService) {\n * this.dataService.getData()\n * .pipe(takeUntilDestroyed())\n * .subscribe(data => console.log(data));\n * }\n * \n * // Usage with explicit DestroyRef\n * constructor(private dataService: DataService) {\n * const destroyRef = inject(DestroyRef);\n * this.dataService.getData()\n * .pipe(takeUntilDestroyed(destroyRef))\n * .subscribe(data => console.log(data));\n * }\n * ```\n */\nexport function takeUntilDestroyed<T>(destroyRef?: DestroyRef) {\n return (source: Observable<T>): Observable<T> => {\n const destroy$ = new Subject<void>();\n \n // Use provided DestroyRef or inject it automatically\n const ref = destroyRef ?? inject(DestroyRef);\n \n // Register cleanup callback\n ref.onDestroy(() => {\n destroy$.next();\n destroy$.complete();\n });\n \n return source.pipe(takeUntil(destroy$));\n };\n}\n\n/**\n * A simplified version of takeUntilDestroyed that always uses automatic injection.\n * This provides the cleanest API for most use cases.\n * \n * @returns An operator function that can be used in a pipe() chain\n * \n * @example\n * ```typescript\n * constructor(private dataService: DataService) {\n * this.dataService.getData()\n * .pipe(untilDestroyed())\n * .subscribe(data => console.log(data));\n * }\n * ```\n */\nexport function untilDestroyed<T>() {\n return takeUntilDestroyed<T>();\n}\n\n/**\n * Type alias for the takeUntilDestroyed operator function\n */\nexport type TakeUntilDestroyed = typeof takeUntilDestroyed; ","import { Injectable, OnDestroy } from '@angular/core';\nimport { Subscription } from 'rxjs';\n\n/**\n * A service that manages multiple RxJS subscriptions and automatically\n * unsubscribes from all of them when the associated component is destroyed.\n * \n * This service implements Angular's OnDestroy interface and should be\n * provided at the component level to ensure proper cleanup.\n * \n * @example\n * ```typescript\n * @Component({\n * selector: 'app-my-component',\n * providers: [SubscriptionManager] // Provide at component level\n * })\n * export class MyComponent implements OnInit {\n * constructor(\n * private dataService: DataService,\n * private subManager: SubscriptionManager\n * ) {}\n * \n * ngOnInit() {\n * // Add subscriptions to the manager\n * this.subManager.add(\n * this.dataService.getStream1().subscribe(data => console.log(data)),\n * this.dataService.getStream2().subscribe(data => console.log(data))\n * );\n * }\n * // No ngOnDestroy needed - the service handles cleanup automatically\n * }\n * ```\n */\n@Injectable()\nexport class SubscriptionManager implements OnDestroy {\n private subscriptions: Set<Subscription> = new Set();\n private isDestroyed = false;\n\n /**\n * Add one or more subscriptions to be managed by this service.\n * All added subscriptions will be automatically unsubscribed when\n * the component is destroyed.\n * \n * @param subscriptions The subscriptions to add\n * @returns The SubscriptionManager instance for method chaining\n * \n * @example\n * ```typescript\n * // Add single subscription\n * this.subManager.add(observable$.subscribe());\n * \n * // Add multiple subscriptions\n * this.subManager.add(\n * observable1$.subscribe(),\n * observable2$.subscribe()\n * );\n * \n * // Method chaining\n * this.subManager\n * .add(observable1$.subscribe())\n * .add(observable2$.subscribe());\n * ```\n */\n add(...subscriptions: Subscription[]): SubscriptionManager {\n if (this.isDestroyed) {\n console.warn('SubscriptionManager: Attempting to add subscriptions after destroy. Subscriptions will be immediately unsubscribed.');\n subscriptions.forEach(sub => sub.unsubscribe());\n return this;\n }\n\n subscriptions.forEach(subscription => {\n if (subscription && !subscription.closed) {\n this.subscriptions.add(subscription);\n }\n });\n\n return this;\n }\n\n /**\n * Remove a specific subscription from management.\n * The subscription will not be unsubscribed automatically.\n * \n * @param subscription The subscription to remove\n * @returns The SubscriptionManager instance for method chaining\n */\n remove(subscription: Subscription): SubscriptionManager {\n this.subscriptions.delete(subscription);\n return this;\n }\n\n /**\n * Manually unsubscribe from all managed subscriptions.\n * This is automatically called during ngOnDestroy.\n */\n unsubscribeAll(): void {\n this.subscriptions.forEach(subscription => {\n if (subscription && !subscription.closed) {\n subscription.unsubscribe();\n }\n });\n this.subscriptions.clear();\n }\n\n /**\n * Get the current number of active subscriptions being managed.\n * \n * @returns The number of active subscriptions\n */\n get activeCount(): number {\n // Filter out closed subscriptions\n const activeSubscriptions = Array.from(this.subscriptions).filter(\n sub => !sub.closed\n );\n \n // Clean up closed subscriptions from the set\n if (activeSubscriptions.length !== this.subscriptions.size) {\n this.subscriptions.clear();\n activeSubscriptions.forEach(sub => this.subscriptions.add(sub));\n }\n \n return activeSubscriptions.length;\n }\n\n /**\n * Check if the manager has any active subscriptions.\n * \n * @returns True if there are active subscriptions, false otherwise\n */\n get hasActiveSubscriptions(): boolean {\n return this.activeCount > 0;\n }\n\n /**\n * Angular lifecycle hook that automatically unsubscribes from all\n * managed subscriptions when the component is destroyed.\n */\n ngOnDestroy(): void {\n this.isDestroyed = true;\n this.unsubscribeAll();\n }\n} ","import { Observable, Subscription, isObservable } from 'rxjs';\nimport { takeUntilDestroyed } from '../operators/take-until-destroyed';\nimport { DestroyRef } from '@angular/core';\n\n/**\n * Type guard to check if a value is a Subscription\n */\nexport function isSubscription(value: any): value is Subscription {\n return value && typeof value.unsubscribe === 'function';\n}\n\n/**\n * Type guard to check if a value is an Observable\n */\nexport function isObservableValue<T>(value: any): value is Observable<T> {\n return isObservable(value);\n}\n\n/**\n * Utility function to safely unsubscribe from a subscription\n * without throwing errors if the subscription is null, undefined, or already closed.\n * \n * @param subscription The subscription to unsubscribe from\n * @returns True if unsubscription was successful, false otherwise\n * \n * @example\n * ```typescript\n * let sub: Subscription | undefined;\n * // ... later\n * safeUnsubscribe(sub); // Won't throw if sub is undefined\n * ```\n */\nexport function safeUnsubscribe(subscription: Subscription | null | undefined): boolean {\n if (subscription && !subscription.closed) {\n try {\n subscription.unsubscribe();\n return true;\n } catch (error) {\n console.warn('Error during unsubscription:', error);\n return false;\n }\n }\n return false;\n}\n\n/**\n * Utility function to create an observable that automatically unsubscribes\n * when the component is destroyed. This is a functional approach alternative\n * to using the operator in a pipe.\n * \n * @param source$ The source observable\n * @param destroyRef Optional DestroyRef instance\n * @returns A new observable that will complete on component destruction\n * \n * @example\n * ```typescript\n * const managedObservable$ = createManagedObservable(\n * this.dataService.getData(),\n * inject(DestroyRef)\n * );\n * \n * managedObservable$.subscribe(data => console.log(data));\n * ```\n */\nexport function createManagedObservable<T>(\n source$: Observable<T>,\n destroyRef?: DestroyRef\n): Observable<T> {\n return source$.pipe(takeUntilDestroyed(destroyRef));\n}\n\n/**\n * Utility function to handle multiple observables with automatic cleanup.\n * Returns an array of managed observables.\n * \n * @param observables Array of source observables\n * @param destroyRef Optional DestroyRef instance\n * @returns Array of managed observables\n * \n * @example\n * ```typescript\n * const [data1$, data2$, data3$] = manageManyObservables([\n * this.service.getData1(),\n * this.service.getData2(),\n * this.service.getData3()\n * ]);\n * ```\n */\nexport function manageManyObservables<T extends readonly Observable<any>[]>(\n observables: T,\n destroyRef?: DestroyRef\n): { [K in keyof T]: T[K] } {\n return observables.map(obs => createManagedObservable(obs, destroyRef)) as any;\n}\n\n/**\n * Configuration options for subscription debugging\n */\nexport interface SubscriptionDebugOptions {\n /** Enable console logging for subscription lifecycle events */\n enableLogging?: boolean;\n /** Custom prefix for log messages */\n logPrefix?: string;\n /** Enable stack trace capture for subscription creation */\n captureStackTrace?: boolean;\n}\n\n/**\n * Utility class for debugging subscription lifecycle in development\n */\nexport class SubscriptionDebugger {\n private static defaultOptions: SubscriptionDebugOptions = {\n enableLogging: false,\n logPrefix: '[ng-terminus]',\n captureStackTrace: false\n };\n\n private static options = { ...SubscriptionDebugger.defaultOptions };\n\n /**\n * Configure global debugging options\n */\n static configure(options: Partial<SubscriptionDebugOptions>): void {\n SubscriptionDebugger.options = {\n ...SubscriptionDebugger.defaultOptions,\n ...options\n };\n }\n\n /**\n * Log subscription creation\n */\n static logSubscription(context: string, subscription?: Subscription): void {\n if (!SubscriptionDebugger.options.enableLogging) return;\n\n const prefix = SubscriptionDebugger.options.logPrefix;\n console.log(`${prefix} Subscription created in ${context}`, {\n subscription,\n timestamp: new Date().toISOString(),\n ...(SubscriptionDebugger.options.captureStackTrace && {\n stack: new Error().stack\n })\n });\n }\n\n /**\n * Log subscription cleanup\n */\n static logCleanup(context: string, count: number): void {\n if (!SubscriptionDebugger.options.enableLogging) return;\n\n const prefix = SubscriptionDebugger.options.logPrefix;\n console.log(`${prefix} Cleaned up ${count} subscription(s) in ${context}`, {\n timestamp: new Date().toISOString()\n });\n }\n}\n\n/**\n * Types for better TypeScript support\n */\n\n/** A function that returns an observable */\nexport type ObservableFactory<T> = () => Observable<T>;\n\n/** A function that handles subscription cleanup */\nexport type CleanupFunction = () => void;\n\n/** Configuration for subscription management */\nexport interface SubscriptionConfig {\n /** Automatically log subscription lifecycle events */\n debug?: boolean;\n /** Custom cleanup functions to run on destroy */\n cleanupFunctions?: CleanupFunction[];\n} ","import { Injectable, DestroyRef, inject } from '@angular/core';\nimport { Observable, Subscription, Subject } from 'rxjs';\nimport { tap, finalize, share } from 'rxjs/operators';\n\nexport interface SubscriptionDebugInfo {\n id: string;\n componentName?: string;\n operatorName?: string;\n createdAt: Date;\n stackTrace?: string;\n isActive: boolean;\n emissionCount: number;\n errorCount: number;\n lastEmission?: Date;\n lastError?: Date;\n memoryUsage?: number;\n duration?: number;\n}\n\nexport interface PerformanceMetrics {\n totalSubscriptions: number;\n activeSubscriptions: number;\n totalEmissions: number;\n totalErrors: number;\n averageLifetime: number;\n memoryUsage: number;\n leaksDetected: number;\n}\n\n@Injectable({\n providedIn: 'root'\n})\nexport class SubscriptionDebuggerService {\n private subscriptions = new Map<string, SubscriptionDebugInfo>();\n private debugCounter = 0;\n private isEnabled = false;\n private performanceObserver?: PerformanceObserver;\n private memoryInterval?: NodeJS.Timeout;\n\n constructor() {\n this.setupPerformanceMonitoring();\n this.setupMemoryMonitoring();\n }\n\n /**\n * Enable debugging mode\n */\n enable(): void {\n this.isEnabled = true;\n console.log('๐Ÿ” SubscriptionDebugger enabled');\n }\n\n /**\n * Disable debugging mode\n */\n disable(): void {\n this.isEnabled = false;\n console.log('๐Ÿ” SubscriptionDebugger disabled');\n }\n\n /**\n * Create a debuggable subscription with detailed tracking\n */\n debugSubscription<T>(\n source: Observable<T>,\n options: {\n name?: string;\n componentName?: string;\n captureStackTrace?: boolean;\n logEmissions?: boolean;\n logErrors?: boolean;\n } = {}\n ): Observable<T> {\n if (!this.isEnabled) {\n return source;\n }\n\n const id = `debug_${++this.debugCounter}`;\n const info: SubscriptionDebugInfo = {\n id,\n componentName: options.componentName,\n operatorName: options.name || 'Unknown',\n createdAt: new Date(),\n isActive: true,\n emissionCount: 0,\n errorCount: 0,\n stackTrace: options.captureStackTrace ? this.captureStackTrace() : undefined\n };\n\n this.subscriptions.set(id, info);\n\n return source.pipe(\n tap({\n next: (value) => {\n info.emissionCount++;\n info.lastEmission = new Date();\n \n if (options.logEmissions) {\n console.log(`๐Ÿ“ฆ [${id}] ${info.operatorName}:`, value);\n }\n },\n error: (error) => {\n info.errorCount++;\n info.lastError = new Date();\n \n if (options.logErrors) {\n console.error(`โŒ [${id}] ${info.operatorName} Error:`, error);\n }\n },\n complete: () => {\n if (options.logEmissions) {\n console.log(`โœ… [${id}] ${info.operatorName} completed`);\n }\n }\n }),\n finalize(() => {\n info.isActive = false;\n info.duration = Date.now() - info.createdAt.getTime();\n \n // Clean up after some time to prevent memory leaks\n setTimeout(() => {\n this.subscriptions.delete(id);\n }, 60000); // Keep debug info for 1 minute after completion\n }),\n share()\n );\n }\n\n /**\n * Get detailed information about a specific subscription\n */\n getSubscriptionInfo(id: string): SubscriptionDebugInfo | undefined {\n return this.subscriptions.get(id);\n }\n\n /**\n * Get all active subscriptions\n */\n getActiveSubscriptions(): SubscriptionDebugInfo[] {\n return Array.from(this.subscriptions.values()).filter(info => info.isActive);\n }\n\n /**\n * Get all subscriptions (active and completed)\n */\n getAllSubscriptions(): SubscriptionDebugInfo[] {\n return Array.from(this.subscriptions.values());\n }\n\n /**\n * Get performance metrics\n */\n getPerformanceMetrics(): PerformanceMetrics {\n const all = this.getAllSubscriptions();\n const active = this.getActiveSubscriptions();\n \n return {\n totalSubscriptions: all.length,\n activeSubscriptions: active.length,\n totalEmissions: all.reduce((sum, info) => sum + info.emissionCount, 0),\n totalErrors: all.reduce((sum, info) => sum + info.errorCount, 0),\n averageLifetime: this.calculateAverageLifetime(all),\n memoryUsage: this.getCurrentMemoryUsage(),\n leaksDetected: this.detectPotentialLeaks()\n };\n }\n\n /**\n * Log current subscription status\n */\n logStatus(): void {\n if (!this.isEnabled) {\n console.log('๐Ÿ” SubscriptionDebugger is disabled');\n return;\n }\n\n const metrics = this.getPerformanceMetrics();\n const active = this.getActiveSubscriptions();\n\n console.group('๐Ÿ” Subscription Debugger Status');\n console.log('๐Ÿ“Š Metrics:', metrics);\n \n if (active.length > 0) {\n console.group('๐Ÿ”ด Active Subscriptions:');\n active.forEach(info => {\n console.log(`[${info.id}] ${info.operatorName} (${info.componentName || 'Unknown'})`, {\n emissions: info.emissionCount,\n errors: info.errorCount,\n age: Date.now() - info.createdAt.getTime(),\n stackTrace: info.stackTrace\n });\n });\n console.groupEnd();\n }\n\n if (metrics.leaksDetected > 0) {\n console.warn(`โš ๏ธ Potential memory leaks detected: ${metrics.leaksDetected}`);\n }\n\n console.groupEnd();\n }\n\n /**\n * Detect potential memory leaks\n */\n detectPotentialLeaks(): number {\n const active = this.getActiveSubscriptions();\n const now = Date.now();\n \n return active.filter(info => {\n const age = now - info.createdAt.getTime();\n // Consider subscriptions older than 5 minutes with no recent activity as potential leaks\n return age > 300000 && (!info.lastEmission || (now - info.lastEmission.getTime()) > 60000);\n }).length;\n }\n\n /**\n * Clear all debug information\n */\n clear(): void {\n this.subscriptions.clear();\n console.log('๐Ÿงน Subscription debug information cleared');\n }\n\n /**\n * Export debug information as JSON\n */\n exportDebugInfo(): string {\n const data = {\n timestamp: new Date().toISOString(),\n metrics: this.getPerformanceMetrics(),\n subscriptions: this.getAllSubscriptions()\n };\n \n return JSON.stringify(data, null, 2);\n }\n\n private captureStackTrace(): string {\n const error = new Error();\n return error.stack || 'Stack trace not available';\n }\n\n private calculateAverageLifetime(subscriptions: SubscriptionDebugInfo[]): number {\n const completed = subscriptions.filter(info => !info.isActive && info.duration);\n if (completed.length === 0) return 0;\n \n const total = completed.reduce((sum, info) => sum + (info.duration || 0), 0);\n return total / completed.length;\n }\n\n private getCurrentMemoryUsage(): number {\n if (typeof performance !== 'undefined' && 'memory' in performance) {\n return (performance as any).memory.usedJSHeapSize;\n }\n return 0;\n }\n\n private setupPerformanceMonitoring(): void {\n if (typeof PerformanceObserver !== 'undefined') {\n this.performanceObserver = new PerformanceObserver((list) => {\n const entries = list.getEntries();\n // Process performance entries if needed\n });\n \n try {\n this.performanceObserver.observe({ entryTypes: ['measure', 'navigation'] });\n } catch (error) {\n console.warn('Performance monitoring not available:', error);\n }\n }\n }\n\n private setupMemoryMonitoring(): void {\n if (this.isEnabled && typeof performance !== 'undefined' && 'memory' in performance) {\n this.memoryInterval = setInterval(() => {\n const memory = (performance as any).memory;\n if (memory.usedJSHeapSize > memory.jsHeapSizeLimit * 0.9) {\n console.warn('โš ๏ธ High memory usage detected:', {\n used: memory.usedJSHeapSize,\n limit: memory.jsHeapSizeLimit,\n percentage: (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100\n });\n }\n }, 30000); // Check every 30 seconds\n }\n }\n\n ngOnDestroy(): void {\n if (this.performanceObserver) {\n this.performanceObserver.disconnect();\n }\n \n if (this.memoryInterval) {\n clearInterval(this.memoryInterval);\n }\n }\n} ","import { inject, DestroyRef } from '@angular/core';\nimport { HttpClient, HttpRequest, HttpResponse } from '@angular/common/http';\nimport { Observable, Subject, throwError } from 'rxjs';\nimport { takeUntil, catchError, tap, finalize, retry, delay } from 'rxjs/operators';\n\n/**\n * Service to manage HTTP request cancellation\n */\nexport class HttpRequestManager {\n private pendingRequests = new Map<string, Subject<void>>();\n private requestCounter = 0;\n\n /**\n * Create a cancellable HTTP request\n */\n createCancellableRequest<T>(\n requestFn: () => Observable<T>,\n requestId?: string\n ): { request$: Observable<T>; cancel: () => void } {\n const id = requestId || `req_${++this.requestCounter}`;\n const cancelSubject = new Subject<void>();\n \n this.pendingRequests.set(id, cancelSubject);\n\n const request$ = requestFn().pipe(\n takeUntil(cancelSubject),\n catchError((error) => {\n if (error.name === 'AbortError') {\n console.log(`Request ${id} was cancelled`);\n return throwError(() => new Error('Request cancelled'));\n }\n return throwError(() => error);\n }),\n finalize(() => {\n this.pendingRequests.delete(id);\n })\n );\n\n const cancel = () => {\n cancelSubject.next();\n cancelSubject.complete();\n this.pendingRequests.delete(id);\n };\n\n return { request$, cancel };\n }\n\n /**\n * Cancel a specific request by ID\n */\n cancelRequest(requestId: string): void {\n const cancelSubject = this.pendingRequests.get(requestId);\n if (cancelSubject) {\n cancelSubject.next();\n cancelSubject.complete();\n this.pendingRequests.delete(requestId);\n }\n }\n\n /**\n * Cancel all pending requests\n */\n cancelAllRequests(): void {\n this.pendingRequests.forEach((cancelSubject) => {\n cancelSubject.next();\n cancelSubject.complete();\n });\n this.pendingRequests.clear();\n }\n\n /**\n * Get the number of pending requests\n */\n getPendingRequestCount(): number {\n return this.pendingRequests.size;\n }\n\n /**\n * Get all pending request IDs\n */\n getPendingRequestIds(): string[] {\n return Array.from(this.pendingRequests.keys());\n }\n}\n\n/**\n * RxJS operator that cancels HTTP requests when component is destroyed\n */\nexport function cancelOnDestroy() {\n return function <T>(source: Observable<T>): Observable<T> {\n const destroyRef = inject(DestroyRef, { optional: true });\n \n if (!destroyRef) {\n console.warn('cancelOnDestroy: DestroyRef not available, operator will have no effect');\n return source;\n }\n\n const destroy$ = new Subject<void>();\n destroyRef.onDestroy(() => {\n destroy$.next();\n destroy$.complete();\n });\n\n return source.pipe(takeUntil(destroy$));\n };\n}\n\n/**\n * RxJS operator that cancels previous HTTP requests when a new one is made\n */\nexport function cancelPrevious() {\n let currentRequest$: Subject<void> | null = null;\n\n return function <T>(source: Observable<T>): Observable<T> {\n // Cancel previous request\n if (currentRequest$) {\n currentRequest$.next();\n currentRequest$.complete();\n }\n\n // Create new cancellation subject\n currentRequest$ = new Subject<void>();\n const cancelSubject = currentRequest$;\n\n return source.pipe(\n takeUntil(cancelSubject),\n finalize(() => {\n if (currentRequest$ === cancelSubject) {\n currentRequest$ = null;\n }\n })\n );\n };\n}\n\n/**\n * RxJS operator that adds retry logic with exponential backoff for HTTP requests\n */\nexport function retryWithBackoff(\n maxRetries: number = 3,\n initialDelay: number = 1000,\n maxDelay: number = 30000\n) {\n return function <T>(source: Observable<T>): Observable<T> {\n return source.pipe(\n catchError((error, caught) => {\n if (maxRetries <= 0) {\n return throwError(() => error);\n }\n\n const delayTime = Math.min(initialDelay * Math.pow(2, 3 - maxRetries), maxDelay);\n \n return new Observable<T>(subscriber => {\n const timeoutId = setTimeout(() => {\n const retriedObservable = caught.pipe(\n retryWithBackoff(maxRetries - 1, initialDelay, maxDelay)\n ) as Observable<T>;\n retriedObservable.subscribe(subscriber);\n }, delayTime);\n\n return () => clearTimeout(timeoutId);\n });\n })\n );\n };\n}\n\n/**\n * RxJS operator that logs HTTP request lifecycle events\n */\nexport function logHttpRequests(requestName?: string) {\n return function <T>(source: Observable<T>): Observable<T> {\n const name = requestName || 'HTTP Request';\n \n return source.pipe(\n tap({\n subscribe: () => console.log(`๐Ÿš€ ${name}: Started`),\n next: (value) => console.log(`๐Ÿ“ฆ ${name}: Received data`, value),\n error: (error) => console.error(`โŒ ${name}: Error`, error),\n complete: () => console.log(`โœ… ${name}: Completed`)\n })\n );\n };\n} ","import { inject, DestroyRef, Injectable } from '@angular/core';\nimport { Observable, Subject } from 'rxjs';\nimport { takeUntil, filter } from 'rxjs/operators';\n\n/**\n * RxJS operator that automatically unsubscribes from form value changes when component is destroyed\n */\nexport function takeUntilFormDestroyed() {\n return function <T>(source: Observable<T>): Observable<T> {\n const destroyRef = inject(DestroyRef, { optional: true });\n \n if (!destroyRef) {\n console.warn('takeUntilFormDestroyed: DestroyRef not available, operator will have no effect');\n return source;\n }\n\n const destroy$ = new Subject<void>();\n destroyRef.onDestroy(() => {\n destroy$.next();\n destroy$.complete();\n });\n\n return source.pipe(takeUntil(destroy$));\n };\n}\n\n/**\n * RxJS operator that emits only when form is valid\n */\nexport function takeWhileFormValid<T>(isValid: () => boolean) {\n return function (source: Observable<T>): Observable<T> {\n return source.pipe(\n filter(() => isValid())\n );\n };\n}\n\n/**\n * Injectable service for managing form subscriptions\n */\n@Injectable()\nexport class FormSubscriptionManager {\n private subscriptions = new Map<string, Subject<void>>();\n\n /**\n * Create a managed subscription with automatic cleanup\n */\n manage<T>(source: Observable<T>, name: string): Observable<T> {\n const destroy$ = new Subject<void>();\n this.subscriptions.set(name, destroy$);\n \n return source.pipe(takeUntil(destroy$));\n }\n\n /**\n * Unsubscribe from a specific managed subscription\n */\n unsubscribe(name: string): void {\n const destroy$ = this.subscriptions.get(name);\n if (destroy$) {\n destroy$.next();\n destroy$.complete();\n this.subscriptions.delete(name);\n }\n }\n\n /**\n * Unsubscribe from all managed subscriptions\n */\n unsubscribeAll(): void {\n this.subscriptions.forEach(destroy$ => {\n destroy$.next();\n destroy$.complete();\n });\n this.subscriptions.clear();\n }\n\n /**\n * Get count of active subscriptions\n */\n getActiveCount(): number {\n return this.subscriptions.size;\n }\n} ","import { NgModule, ModuleWithProviders } from '@angular/core';\nimport { SubscriptionManager } from './services/subscription-manager.service';\nimport { SubscriptionDebuggerService } from './services/subscription-debugger.service';\nimport { HttpRequestManager } from './operators/http-operators';\nimport { FormSubscriptionManager } from './operators/forms-operators';\nimport { MemoryOptimizer } from './utils/memory-optimization';\n\n/**\n * Configuration options for NgTerminus\n */\nexport interface NgTerminusConfig {\n enableDebugger?: boolean;\n enableMemoryOptimization?: boolean;\n debugMode?: boolean;\n}\n\n/**\n * The main Angular module for ng-terminus library.\n * \n * This module can be imported into your Angular application to provide\n * all ng-terminus services and enable comprehensive subscription management.\n * \n * @example\n * ```typescript\n * import { NgModule } from '@angular/core';\n * import { NgTerminusModule } from '@fivexlabs/ng-terminus';\n * \n * @NgModule({\n * imports: [NgTerminusModule.forRoot({ enableDebugger: true })],\n * // ...\n * })\n * export class AppModule { }\n * ```\n */\n@NgModule({\n providers: [\n SubscriptionManager,\n SubscriptionDebuggerService,\n HttpRequestManager,\n FormSubscriptionManager\n ]\n})\nexport class NgTerminusModule {\n /**\n * Use this method to configure the module for the root application.\n * \n * @param config Configuration options for ng-terminus\n * @returns The configured module with providers\n * \n * @example\n * ```typescript\n * @NgModule({\n * imports: [NgTerminusModule.forRoot({\n * enableDebugger: true,\n * enableMemoryOptimization: true,\n * debugMode: environment.production === false\n * })],\n * // ...\n * })\n * export class AppModule { }\n * ```\n */\n static forRoot(config: NgTerminusConfig = {}): ModuleWithProviders<NgTerminusModule> {\n return {\n ngModule: NgTerminusModule,\n providers: [\n SubscriptionManager,\n SubscriptionDebuggerService,\n HttpRequestManager,\n FormSubscriptionManager,\n {\n provide: 'NG_TERMINUS_CONFIG',\n useValue: config\n }\n ]\n };\n }\n\n /**\n * Use this method to configure the module for feature modules.\n * This ensures proper service scoping in lazy-loaded modules.\n * \n * @param config Optional configuration for feature modules\n * @returns The configured module\n * \n * @example\n * ```typescript\n * @NgModule({\n * imports: [NgTerminusModule.forFeature()],\n * // ...\n * })\n * export class FeatureModule { }\n * ```\n */\n static forFeature(config: Partial<NgTerminusConfig> = {}): ModuleWithProviders<NgTerminusModule> {\n return {\n ngModule: NgTerminusModule,\n providers: [\n // Feature modules get their own instances of these services\n SubscriptionManager,\n HttpRequestManager,\n FormSubscriptionManager\n ]\n };\n }\n\n constructor() {\n // Initialize services if configuration is provided\n // This would typically be done in an APP_INITIALIZER\n console.log('๐ŸŽฏ NgTerminus module initialized');\n }\n} ","import { inject } from '@angular/core';\nimport { Router, NavigationEnd } from '@angular/router';\nimport { Observable, EMPTY } from 'rxjs';\nimport { takeUntil, filter, switchMap } from 'rxjs/operators';\n\n/**\n * RxJS operator that automatically unsubscribes when navigating away from the current route.\n * This is useful for subscriptions that should only remain active while on a specific route.\n * \n * @param targetRoute Optional specific route to monitor. If not provided, monitors any route change.\n * @returns MonoTypeOperatorFunction that unsubscribes on route change\n * \n * @example\n * ```typescript\n * // Unsubscribe on any route change\n * this.dataService.getData()\n * .pipe(takeUntilRoute())\n * .subscribe(data => console.log(data));\n * \n * // Unsubscribe only when leaving specific route\n * this.dataService.getData()\n * .pipe(takeUntilRoute('/dashboard'))\n * .subscribe(data => console.log(data));\n * ```\n */\nexport function takeUntilRoute(targetRoute?: string) {\n return function <T>(source: Observable<T>): Observable<T> {\n const router = inject(Router, { optional: true });\n \n if (!router) {\n console.warn('takeUntilRoute: Router not available, operator will have no effect');\n return source;\n }\n\n const routeChange$ = router.events.pipe(\n filter(event => event instanceof NavigationEnd),\n filter((event: NavigationEnd) => {\n if (!targetRoute) {\n return true; // Unsubscribe on any route change\n }\n // Unsubscribe when leaving the target route\n return !event.urlAfterRedirects.startsWith(targetRoute);\n })\n );\n\n return source.pipe(takeUntil(routeChange$));\n };\n}\n\n/**\n * RxJS operator that keeps subscription active only while on a specific route.\n * When navigating away, it unsubscribes and returns EMPTY.\n * When navigating back, it resubscribes.\n * \n * @param routePattern The route pattern to match (supports wildcards)\n * @returns OperatorFunction that manages subscription based on route presence\n * \n * @example\n * ```typescript\n * this.dataService.getLiveData()\n * .pipe(takeWhileOnRoute('/dashboard/**'))\n * .subscribe(data => console.log('Dashboard data:', data));\n * ```\n */\nexport function takeWhileOnRoute<T>(routePattern: string) {\n return function (source: Observable<T>): Observable<T> {\n const router = inject(Router, { optional: true });\n \n if (!router) {\n console.warn('takeWhileOnRoute: Router not available, operator will have no effect');\n return source;\n }\n\n return router.events.pipe(\n filter(event => event instanceof NavigationEnd),\n switchMap((event: NavigationEnd) => {\n const currentUrl = event.urlAfterRedirects;\n const matches = matchesRoutePattern(currentUrl, routePattern);\n return matches ? source : EMPTY;\n })\n );\n };\n}\n\n/**\n * Utility function to check if a URL matches a route pattern.\n * Supports wildcards (* and **).\n */\nfunction matchesRoutePattern(url: string, pattern: string): boolean {\n // Convert pattern to regex\n const regexPattern = pattern\n .replace(/\\*\\*/g, '.*') // ** matches anything including /\n .replace(/\\*/g, '[^/]*') // * matches anything except /\n .replace(/\\//g, '\\\\/'); // Escape forward slashes\n \n const regex = new RegExp(`^${regexPattern}$`);\n return regex.test(url);\n} ","import { inject, DestroyRef, DOCUMENT } from '@angular/core';\nimport { Observable, fromEvent, merge, EMPTY } from 'rxjs';\nimport { switchMap, takeUntil, startWith, map, distinctUntilChanged, share } from 'rxjs/operators';\n\n/**\n * RxJS operator that pauses subscription when page becomes hidden\n * and resumes when page becomes visible again.\n * \n * @param emitOnResume Whether to emit the last value when resuming (default: true)\n * @returns OperatorFunction that manages subscription based on page visibility\n * \n * @example\n * ```typescript\n * this.dataService.getLiveData()\n * .pipe(takeWhileVisible())\n * .subscribe(data => console.log('Received while visible:', data));\n * ```\n */\nexport function takeWhileVisible<T>(emitOnResume: boolean = true) {\n return function (source: Observable<T>): Observable<T> {\n const document = inject(DOCUMENT, { optional: true });\n \n if (!document) {\n console.warn('takeWhileVisible: Document not available, operator will have no effect');\n return source;\n }\n\n const visibility$ = merge(\n fromEvent(document, 'visibilitychange'),\n fromEvent(document, 'blur'),\n fromEvent(document, 'focus')\n ).pipe(\n startWith(null),\n map(() => !document.hidden),\n distinctUntilChanged(),\n share()\n );\n\n return visibility$.pipe(\n switchMap(isVisible => {\n if (isVisible) {\n return source;\n } else {\n return EMPTY;\n }\n })\n );\n };\n}\n\n/**\n * RxJS operator that unsubscribes when page becomes hidden\n * and doesn't automatically resubscribe when visible again.\n * \n * @returns MonoTypeOperatorFunction that unsubscribes on page hide\n * \n * @example\n * ```typescript\n * this.dataService.getData()\n * .pipe(takeUntilHidden())\n * .subscribe(data => console.log('Data received:', data));\n * ```\n */\nexport function takeUntilHidden<T>() {\n return function (source: Observable<T>): Observable<T> {\n const document = inject(DOCUMENT, { optional: true });\n \n if (!document) {\n console.warn('takeUntilHidden: Document not available, operator will have no effect');\n return source;\n }\n\n const hidden$ = merge(\n fromEvent(document, 'visibilitychange'),\n fromEvent(document, 'blur')\n ).pipe(\n map(() => document.hidden),\n distinctUntilChanged(),\n switchMap(isHidden => isHidden ? [true] : EMPTY)\n );\n\n return source.pipe(takeUntil(hidden$));\n };\n}\n\n/**\n * RxJS operator that emits only when page is visible\n * and buffers emissions while hidden.\n * \n * @param bufferSize Maximum number of emissions to buffer (default: 10)\n * @returns OperatorFunction that buffers emissions while page is hidden\n * \n * @example\n * ```typescript\n * this.dataService.getNotifications()\n * .pipe(bufferWhileHidden(5))\n * .subscribe(notifications => {\n * // Receive up to 5 buffered notifications when page becomes visible\n * console.log('Notifications:', notifications);\n * });\n * ```\n */\nexport function bufferWhileHidden<T>(bufferSize: number = 10) {\n return function (source: Observable<T>): Observable<T[]> {\n const document = inject(DOCUMENT, { optional: true });\n \n if (!document) {\n console.warn('bufferWhileHidden: Document not available, operator will have no effect');\n return source.pipe(map(value => [value]));\n }\n\n const visibility$ = merge(\n fromEvent(document, 'visibilitychange'),\n fromEvent(document, 'focus'),\n fromEvent(document, 'blur')\n ).pipe(\n startWith(null),\n map(() => !document.hidden),\n distinctUntilChanged(),\n share()\n );\n\n let buffer: T[] = [];\n\n return source.pipe(\n switchMap(value => {\n return visibility$.pipe(\n map(isVisible => {\n if (isVisible) {\n // Page is visible, emit buffered values plus current value\n buffer.push(value);\n const result = buffer.slice(-bufferSize);\n buffer = [];\n return result;\n } else {\n // Page is hidden, add to buffer\n buffer.push(value);\n if (buffer.length > bufferSize) {\n buffer = buffer.slice(-bufferSize);\n }\n return [];\n }\n })\n );\n }),\n switchMap(values => values.length > 0 ? [values] : EMPTY)\n );\n };\n}\n\n/**\n * RxJS operator that throttles emissions when page is not visible\n * and resumes normal emission rate when visible.\n * \n * @param hiddenThrottleMs Throttle time in milliseconds when hidden (default: 30000)\n * @returns MonoTypeOperatorFunction that throttles based on visibility\n * \n * @example\n * ```typescript\n * this.dataService.getHeartbeat()\n * .pipe(throttleWhileHidden(60000)) // Throttle to 1 minute when hidden\n * .subscribe(heartbeat => console.log('Heartbeat:', heartbeat));\n * ```\n */\nexport function throttleWhileHidden<T>(hiddenThrottleMs: number = 30000) {\n return function (source: Observable<T>): Observable<T> {\n const document = inject(DOCUMENT, { optional: true });\n \n if (!document) {\n console.warn('throttleWhileHidden: Document not available, operator will have no effect');\n return source;\n }\n\n const visibility$ = merge(\n fromEvent(document, 'visibilitychange'),\n fromEvent(document, 'focus'),\n fromEvent(document, 'blur')\n ).pipe(\n startWith(null),\n map(() => !document.hidden),\n distinctUntilChanged(),\n share()\n );\n\n let lastEmission = 0;\n\n return source.pipe(\n switchMap(value => {\n return visibility$.pipe(\n map(isVisible => {\n const now = Date.now();\n \n if (isVisible) {\n // Always emit when visible\n lastEmission = now;\n return value;\n } else {\n // Throttle when hidden\n if (now - lastEmission >= hiddenThrottleMs) {\n lastEmission = now;\n return value;\n }\n return null;\n }\n })\n );\n }),\n switchMap(value => value !== null ? [value] : EMPTY)\n );\n };\n} ","import { Observable, Subject, Subscription, throwError, EMPTY } from 'rxjs';\nimport { delay, take, tap } from 'rxjs/operators';\n\n/**\n * Mock subscription manager for testing\n */\nexport class MockSubscriptionManager {\n private subscriptions = new Map<string, Subscription>();\n private subscriptionCount = 0;\n\n add(subscription: Subscription, name?: string): string {\n const id = name || `mock_${++this.subscriptionCount}`;\n this.subscriptions.set(id, subscription);\n return id;\n }\n\n remove(id: string): boolean {\n const subscription = this.subscriptions.get(id);\n if (subscription) {\n subscription.unsubscribe();\n this.subscriptions.delete(id);\n return true;\n }\n return false;\n }\n\n removeAll(): void {\n this.subscriptions.forEach(sub => sub.unsubscribe());\n this.subscriptions.clear();\n }\n\n getActiveCount(): number {\n return Array.from(this.subscriptions.values())\n .filter(sub => !sub.closed).length;\n }\n\n getAllCount(): number {\n return this.subscriptions.size;\n }\n\n isActive(id: string): boolean {\n const subscription = this.subscriptions.get(id);\n return subscription ? !subscription.closed : false;\n }\n}\n\n/**\n * Test observable that can be controlled for testing subscription behavior\n */\nexport class TestObservable<T> extends Observable<T> {\n private subject = new Subject<T>();\n private isCompleted = false;\n private hasErrored = false;\n\n constructor() {\n super(subscriber => {\n const subscription = this.subject.subscribe(subscriber);\n return () => subscription.unsubscribe();\n });\n }\n\n /**\n * Emit a value to all subscribers\n */\n emit(value: T): void {\n if (!this.isCompleted && !this.hasErrored) {\n this.subject.next(value);\n }\n }\n\n /**\n * Emit multiple values with optional delays\n */\n emitSequence(values: T[], delayMs: number = 0): Promise<void> {\n return new Promise((resolve) => {\n let index = 0;\n const emitNext = () => {\n if (index < values.length && !this.isCompleted && !this.hasErrored) {\n this.emit(values[index++]);\n if (index < values.length) {\n setTimeout(emitNext, delayMs);\n } else {\n resolve();\n }\n } else {\n resolve();\n }\n };\n emitNext();\n });\n }\n\n /**\n * Complete the observable\n */\n complete(): void {\n if (!this.isCompleted && !this.hasErrored) {\n this.isCompleted = true;\n this.subject.complete();\n }\n }\n\n /**\n * Emit an error\n */\n error(error: any): void {\n if (!this.isCompleted && !this.hasErrored) {\n this.hasErrored = true;\n this.subject.error(error);\n }\n }\n\n /**\n * Get the current state\n */\n getState(): { completed: boolean; errored: boolean } {\n return {\n completed: this.isCompleted,\n errored: this.hasErrored\n };\n }\n}\n\n/**\n * Subscription testing utilities\n */\nexport class SubscriptionTester {\n private subscriptions: Subscription[] = [];\n private emissionCounts = new Map<string, number>();\n private errorCounts = new Map<string, number>();\n private completionCounts = new Map<string, number>();\n\n /**\n * Subscribe to an observable with tracking\n */\n subscribe<T>(\n observable: Observable<T>,\n name: string,\n options: {\n onNext?: (value: T) => void;\n onError?: (error: any) => void;\n onComplete?: () => void;\n } = {}\n ): Subscription {\n const subscription = observable\n .pipe(\n tap({\n next: (value) => {\n this.incrementCount(this.emissionCounts, name);\n options.onNext?.(value);\n },\n error: (error) => {\n this.incrementCount(this.errorCounts, name);\n options.onError?.(error);\n },\n complete: () => {\n this.incrementCount(this.completionCounts, name);\n options.onComplete?.();\n }\n })\n )\n .subscribe();\n\n this.subscriptions.push(subscription);\n return subscription;\n }\n\n /**\n * Get emission count for a named subscription\n */\n getEmissionCount(name: string): number {\n return this.emissionCounts.get(name) || 0;\n }\n\n /**\n * Get error count for a named subscription\n */\n getErrorCount(name: string): number {\n return this.errorCounts.get(name) || 0;\n }\n\n /**\n * Get completion count for a named subscription\n */\n getCompletionCount(name: string): number {\n return this.completionCounts.get(name) || 0;\n }\n\n /**\n * Get total active subscriptions\n */\n getActiveSubscriptionCount(): number {\n return this.subscriptions.filter(sub => !sub.closed).length;\n }\n\n /**\n * Unsubscribe from all tracked subscriptions\n */\n unsubscribeAll(): void {\n this.subscriptions.forEach(sub => sub.unsubscribe());\n this.subscriptions = [];\n }\n\n /**\n * Reset all counters\n */\n reset(): void {\n this.unsubscribeAll();\n this.emissionCounts.clear();\n this.errorCounts.clear();\n this.completionCounts.clear();\n }\n\n /**\n * Wait for a specific number of emissions\n */\n waitForEmissions(name: string, count: number, timeoutMs: number = 5000): Promise<void> {\n return new Promise((resolve, reject) => {\n const startTime = Date.now();\n const checkEmissions = () => {\n if (this.getEmissionCount(name) >= count) {\n resolve();\n } else if (Date.now() - startTime > timeoutMs) {\n reject(new Error(`Timeout waiting for ${count} emissions from ${name}. Got ${this.getEmissionCount(name)}`));\n } else {\n setTimeout(checkEmissions, 10);\n }\n };\n checkEmissions();\n });\n }\n\n /**\n * Wait for completion\n */\n waitForCompletion(name: string, timeoutMs: number = 5000): Promise<void> {\n return new Promise((resolve, reject) => {\n const startTime = Date.now();\n const checkCompletion = () => {\n if (this.getCompletionCount(name) > 0) {\n resolve();\n } else if (Date.now() - startTime > timeoutMs) {\n reject(new Error(`Timeout waiting for completion of ${name}`));\n } else {\n setTimeout(checkCompletion, 10);\n }\n };\n checkCompletion();\n });\n }\n\n private incrementCount(map: Map<string, number>, key: string): void {\n map.set(key, (map.get(key) || 0) + 1);\n }\n}\n\n/**\n * Memory leak detector for testing\n */\nexport class MemoryLeakDetector {\n private initialMemory?: number;\n private subscriptions: Subscription[] = [];\n\n /**\n * Start monitoring memory usage\n */\n startMonitoring(): void {\n if (typeof performance !== 'undefined' && 'memory' in performance) {\n this.initialMemory = (performance as any).memory.usedJSHeapSize;\n }\n }\n\n /**\n * Add subscription to monitor\n */\n track(subscription: Subscription): void {\n this.subscriptions.push(subscription);\n }\n\n /**\n * Check for memory leaks\n */\n checkForLeaks(): {\n hasLeaks: boolean;\n memoryIncrease: number;\n activeSubscriptions: number;\n totalSubscriptions: number;\n } {\n const currentMemory = typeof performance !== 'undefined' && 'memory' in performance\n ? (performance as any).memory.usedJSHeapSize\n : 0;\n\n const memoryIncrease = this.initialMemory\n ? currentMemory - this.initialMemory\n : 0;\n\n const activeSubscriptions = this.subscriptions.filter(sub => !sub.closed).length;\n\n return {\n hasLeaks: activeSubscriptions > 0 || memoryIncrease > 1000000, // 1MB threshold\n memoryIncrease,\n activeSubscriptions,\n totalSubscriptions: this.subscriptions.length\n };\n }\n\n /**\n * Clean up all tracked subscriptions\n */\n cleanup(): void {\n this.subscriptions.forEach(sub => {\n if (!sub.closed) {\n sub.unsubscribe();\n }\n });\n this.subscriptions = [];\n }\n}\n\n/**\n * Helper functions for creating test scenarios\n */\nexport const TestScenarios = {\n /**\n * Create an observable that emits values at intervals\n */\n createIntervalObservable(intervalMs: number, maxValues: number = 10): Observable<number> {\n return new Observable(subscriber => {\n let count = 0;\n const interval = setInterval(() => {\n if (count < maxValues) {\n subscriber.next(count++);\n } else {\n subscriber.complete();\n cle