@fivexlabs/ng-terminus
Version:
A comprehensive Angular library for managing RxJS subscriptions and preventing memory leaks with advanced features
850 lines (837 loc) • 26.6 kB
TypeScript
import * as i0 from '@angular/core';
import { DestroyRef, OnDestroy, ModuleWithProviders } from '@angular/core';
import { Observable, Subscription, BehaviorSubject, ReplaySubject } from 'rxjs';
/**
* An RxJS operator that automatically completes the source observable
* when the associated Angular component, directive, or service is destroyed.
*
* This operator prevents memory leaks by tying the observable lifecycle
* to Angular's component lifecycle through DestroyRef.
*
* @param destroyRef Optional DestroyRef instance. If not provided,
* it will be automatically injected using Angular's inject() function.
* @returns An operator function that can be used in a pipe() chain
*
* @example
* ```typescript
* // Basic usage with automatic DestroyRef injection
* constructor(private dataService: DataService) {
* this.dataService.getData()
* .pipe(takeUntilDestroyed())
* .subscribe(data => console.log(data));
* }
*
* // Usage with explicit DestroyRef
* constructor(private dataService: DataService) {
* const destroyRef = inject(DestroyRef);
* this.dataService.getData()
* .pipe(takeUntilDestroyed(destroyRef))
* .subscribe(data => console.log(data));
* }
* ```
*/
declare function takeUntilDestroyed<T>(destroyRef?: DestroyRef): (source: Observable<T>) => Observable<T>;
/**
* A simplified version of takeUntilDestroyed that always uses automatic injection.
* This provides the cleanest API for most use cases.
*
* @returns An operator function that can be used in a pipe() chain
*
* @example
* ```typescript
* constructor(private dataService: DataService) {
* this.dataService.getData()
* .pipe(untilDestroyed())
* .subscribe(data => console.log(data));
* }
* ```
*/
declare function untilDestroyed<T>(): (source: Observable<T>) => Observable<T>;
/**
* Type alias for the takeUntilDestroyed operator function
*/
type TakeUntilDestroyed = typeof takeUntilDestroyed;
/**
* A service that manages multiple RxJS subscriptions and automatically
* unsubscribes from all of them when the associated component is destroyed.
*
* This service implements Angular's OnDestroy interface and should be
* provided at the component level to ensure proper cleanup.
*
* @example
* ```typescript
* @Component({
* selector: 'app-my-component',
* providers: [SubscriptionManager] // Provide at component level
* })
* export class MyComponent implements OnInit {
* constructor(
* private dataService: DataService,
* private subManager: SubscriptionManager
* ) {}
*
* ngOnInit() {
* // Add subscriptions to the manager
* this.subManager.add(
* this.dataService.getStream1().subscribe(data => console.log(data)),
* this.dataService.getStream2().subscribe(data => console.log(data))
* );
* }
* // No ngOnDestroy needed - the service handles cleanup automatically
* }
* ```
*/
declare class SubscriptionManager implements OnDestroy {
private subscriptions;
private isDestroyed;
/**
* Add one or more subscriptions to be managed by this service.
* All added subscriptions will be automatically unsubscribed when
* the component is destroyed.
*
* @param subscriptions The subscriptions to add
* @returns The SubscriptionManager instance for method chaining
*
* @example
* ```typescript
* // Add single subscription
* this.subManager.add(observable$.subscribe());
*
* // Add multiple subscriptions
* this.subManager.add(
* observable1$.subscribe(),
* observable2$.subscribe()
* );
*
* // Method chaining
* this.subManager
* .add(observable1$.subscribe())
* .add(observable2$.subscribe());
* ```
*/
add(...subscriptions: Subscription[]): SubscriptionManager;
/**
* Remove a specific subscription from management.
* The subscription will not be unsubscribed automatically.
*
* @param subscription The subscription to remove
* @returns The SubscriptionManager instance for method chaining
*/
remove(subscription: Subscription): SubscriptionManager;
/**
* Manually unsubscribe from all managed subscriptions.
* This is automatically called during ngOnDestroy.
*/
unsubscribeAll(): void;
/**
* Get the current number of active subscriptions being managed.
*
* @returns The number of active subscriptions
*/
get activeCount(): number;
/**
* Check if the manager has any active subscriptions.
*
* @returns True if there are active subscriptions, false otherwise
*/
get hasActiveSubscriptions(): boolean;
/**
* Angular lifecycle hook that automatically unsubscribes from all
* managed subscriptions when the component is destroyed.
*/
ngOnDestroy(): void;
static ɵfac: i0.ɵɵFactoryDeclaration<SubscriptionManager, never>;
static ɵprov: i0.ɵɵInjectableDeclaration<SubscriptionManager>;
}
/**
* Type guard to check if a value is a Subscription
*/
declare function isSubscription(value: any): value is Subscription;
/**
* Type guard to check if a value is an Observable
*/
declare function isObservableValue<T>(value: any): value is Observable<T>;
/**
* Utility function to safely unsubscribe from a subscription
* without throwing errors if the subscription is null, undefined, or already closed.
*
* @param subscription The subscription to unsubscribe from
* @returns True if unsubscription was successful, false otherwise
*
* @example
* ```typescript
* let sub: Subscription | undefined;
* // ... later
* safeUnsubscribe(sub); // Won't throw if sub is undefined
* ```
*/
declare function safeUnsubscribe(subscription: Subscription | null | undefined): boolean;
/**
* Utility function to create an observable that automatically unsubscribes
* when the component is destroyed. This is a functional approach alternative
* to using the operator in a pipe.
*
* @param source$ The source observable
* @param destroyRef Optional DestroyRef instance
* @returns A new observable that will complete on component destruction
*
* @example
* ```typescript
* const managedObservable$ = createManagedObservable(
* this.dataService.getData(),
* inject(DestroyRef)
* );
*
* managedObservable$.subscribe(data => console.log(data));
* ```
*/
declare function createManagedObservable<T>(source$: Observable<T>, destroyRef?: DestroyRef): Observable<T>;
/**
* Utility function to handle multiple observables with automatic cleanup.
* Returns an array of managed observables.
*
* @param observables Array of source observables
* @param destroyRef Optional DestroyRef instance
* @returns Array of managed observables
*
* @example
* ```typescript
* const [data1$, data2$, data3$] = manageManyObservables([
* this.service.getData1(),
* this.service.getData2(),
* this.service.getData3()
* ]);
* ```
*/
declare function manageManyObservables<T extends readonly Observable<any>[]>(observables: T, destroyRef?: DestroyRef): {
[K in keyof T]: T[K];
};
/**
* Configuration options for subscription debugging
*/
interface SubscriptionDebugOptions {
/** Enable console logging for subscription lifecycle events */
enableLogging?: boolean;
/** Custom prefix for log messages */
logPrefix?: string;
/** Enable stack trace capture for subscription creation */
captureStackTrace?: boolean;
}
/**
* Utility class for debugging subscription lifecycle in development
*/
declare class SubscriptionDebugger {
private static defaultOptions;
private static options;
/**
* Configure global debugging options
*/
static configure(options: Partial<SubscriptionDebugOptions>): void;
/**
* Log subscription creation
*/
static logSubscription(context: string, subscription?: Subscription): void;
/**
* Log subscription cleanup
*/
static logCleanup(context: string, count: number): void;
}
/**
* Types for better TypeScript support
*/
/** A function that returns an observable */
type ObservableFactory<T> = () => Observable<T>;
/** A function that handles subscription cleanup */
type CleanupFunction = () => void;
/** Configuration for subscription management */
interface SubscriptionConfig {
/** Automatically log subscription lifecycle events */
debug?: boolean;
/** Custom cleanup functions to run on destroy */
cleanupFunctions?: CleanupFunction[];
}
/**
* Configuration options for NgTerminus
*/
interface NgTerminusConfig {
enableDebugger?: boolean;
enableMemoryOptimization?: boolean;
debugMode?: boolean;
}
/**
* The main Angular module for ng-terminus library.
*
* This module can be imported into your Angular application to provide
* all ng-terminus services and enable comprehensive subscription management.
*
* @example
* ```typescript
* import { NgModule } from '@angular/core';
* import { NgTerminusModule } from '@fivexlabs/ng-terminus';
*
* @NgModule({
* imports: [NgTerminusModule.forRoot({ enableDebugger: true })],
* // ...
* })
* export class AppModule { }
* ```
*/
declare class NgTerminusModule {
/**
* Use this method to configure the module for the root application.
*
* @param config Configuration options for ng-terminus
* @returns The configured module with providers
*
* @example
* ```typescript
* @NgModule({
* imports: [NgTerminusModule.forRoot({
* enableDebugger: true,
* enableMemoryOptimization: true,
* debugMode: environment.production === false
* })],
* // ...
* })
* export class AppModule { }
* ```
*/
static forRoot(config?: NgTerminusConfig): ModuleWithProviders<NgTerminusModule>;
/**
* Use this method to configure the module for feature modules.
* This ensures proper service scoping in lazy-loaded modules.
*
* @param config Optional configuration for feature modules
* @returns The configured module
*
* @example
* ```typescript
* @NgModule({
* imports: [NgTerminusModule.forFeature()],
* // ...
* })
* export class FeatureModule { }
* ```
*/
static forFeature(config?: Partial<NgTerminusConfig>): ModuleWithProviders<NgTerminusModule>;
constructor();
static ɵfac: i0.ɵɵFactoryDeclaration<NgTerminusModule, never>;
static ɵmod: i0.ɵɵNgModuleDeclaration<NgTerminusModule, never, never, never>;
static ɵinj: i0.ɵɵInjectorDeclaration<NgTerminusModule>;
}
/**
* RxJS operator that automatically unsubscribes when navigating away from the current route.
* This is useful for subscriptions that should only remain active while on a specific route.
*
* @param targetRoute Optional specific route to monitor. If not provided, monitors any route change.
* @returns MonoTypeOperatorFunction that unsubscribes on route change
*
* @example
* ```typescript
* // Unsubscribe on any route change
* this.dataService.getData()
* .pipe(takeUntilRoute())
* .subscribe(data => console.log(data));
*
* // Unsubscribe only when leaving specific route
* this.dataService.getData()
* .pipe(takeUntilRoute('/dashboard'))
* .subscribe(data => console.log(data));
* ```
*/
declare function takeUntilRoute(targetRoute?: string): <T>(source: Observable<T>) => Observable<T>;
/**
* RxJS operator that keeps subscription active only while on a specific route.
* When navigating away, it unsubscribes and returns EMPTY.
* When navigating back, it resubscribes.
*
* @param routePattern The route pattern to match (supports wildcards)
* @returns OperatorFunction that manages subscription based on route presence
*
* @example
* ```typescript
* this.dataService.getLiveData()
* .pipe(takeWhileOnRoute('/dashboard/**'))
* .subscribe(data => console.log('Dashboard data:', data));
* ```
*/
declare function takeWhileOnRoute<T>(routePattern: string): (source: Observable<T>) => Observable<T>;
/**
* Service to manage HTTP request cancellation
*/
declare class HttpRequestManager {
private pendingRequests;
private requestCounter;
/**
* Create a cancellable HTTP request
*/
createCancellableRequest<T>(requestFn: () => Observable<T>, requestId?: string): {
request$: Observable<T>;
cancel: () => void;
};
/**
* Cancel a specific request by ID
*/
cancelRequest(requestId: string): void;
/**
* Cancel all pending requests
*/
cancelAllRequests(): void;
/**
* Get the number of pending requests
*/
getPendingRequestCount(): number;
/**
* Get all pending request IDs
*/
getPendingRequestIds(): string[];
}
/**
* RxJS operator that cancels HTTP requests when component is destroyed
*/
declare function cancelOnDestroy(): <T>(source: Observable<T>) => Observable<T>;
/**
* RxJS operator that cancels previous HTTP requests when a new one is made
*/
declare function cancelPrevious(): <T>(source: Observable<T>) => Observable<T>;
/**
* RxJS operator that adds retry logic with exponential backoff for HTTP requests
*/
declare function retryWithBackoff(maxRetries?: number, initialDelay?: number, maxDelay?: number): <T>(source: Observable<T>) => Observable<T>;
/**
* RxJS operator that logs HTTP request lifecycle events
*/
declare function logHttpRequests(requestName?: string): <T>(source: Observable<T>) => Observable<T>;
/**
* RxJS operator that pauses subscription when page becomes hidden
* and resumes when page becomes visible again.
*
* @param emitOnResume Whether to emit the last value when resuming (default: true)
* @returns OperatorFunction that manages subscription based on page visibility
*
* @example
* ```typescript
* this.dataService.getLiveData()
* .pipe(takeWhileVisible())
* .subscribe(data => console.log('Received while visible:', data));
* ```
*/
declare function takeWhileVisible<T>(emitOnResume?: boolean): (source: Observable<T>) => Observable<T>;
/**
* RxJS operator that unsubscribes when page becomes hidden
* and doesn't automatically resubscribe when visible again.
*
* @returns MonoTypeOperatorFunction that unsubscribes on page hide
*
* @example
* ```typescript
* this.dataService.getData()
* .pipe(takeUntilHidden())
* .subscribe(data => console.log('Data received:', data));
* ```
*/
declare function takeUntilHidden<T>(): (source: Observable<T>) => Observable<T>;
/**
* RxJS operator that emits only when page is visible
* and buffers emissions while hidden.
*
* @param bufferSize Maximum number of emissions to buffer (default: 10)
* @returns OperatorFunction that buffers emissions while page is hidden
*
* @example
* ```typescript
* this.dataService.getNotifications()
* .pipe(bufferWhileHidden(5))
* .subscribe(notifications => {
* // Receive up to 5 buffered notifications when page becomes visible
* console.log('Notifications:', notifications);
* });
* ```
*/
declare function bufferWhileHidden<T>(bufferSize?: number): (source: Observable<T>) => Observable<T[]>;
/**
* RxJS operator that throttles emissions when page is not visible
* and resumes normal emission rate when visible.
*
* @param hiddenThrottleMs Throttle time in milliseconds when hidden (default: 30000)
* @returns MonoTypeOperatorFunction that throttles based on visibility
*
* @example
* ```typescript
* this.dataService.getHeartbeat()
* .pipe(throttleWhileHidden(60000)) // Throttle to 1 minute when hidden
* .subscribe(heartbeat => console.log('Heartbeat:', heartbeat));
* ```
*/
declare function throttleWhileHidden<T>(hiddenThrottleMs?: number): (source: Observable<T>) => Observable<T>;
interface SubscriptionDebugInfo {
id: string;
componentName?: string;
operatorName?: string;
createdAt: Date;
stackTrace?: string;
isActive: boolean;
emissionCount: number;
errorCount: number;
lastEmission?: Date;
lastError?: Date;
memoryUsage?: number;
duration?: number;
}
interface PerformanceMetrics {
totalSubscriptions: number;
activeSubscriptions: number;
totalEmissions: number;
totalErrors: number;
averageLifetime: number;
memoryUsage: number;
leaksDetected: number;
}
declare class SubscriptionDebuggerService {
private subscriptions;
private debugCounter;
private isEnabled;
private performanceObserver?;
private memoryInterval?;
constructor();
/**
* Enable debugging mode
*/
enable(): void;
/**
* Disable debugging mode
*/
disable(): void;
/**
* Create a debuggable subscription with detailed tracking
*/
debugSubscription<T>(source: Observable<T>, options?: {
name?: string;
componentName?: string;
captureStackTrace?: boolean;
logEmissions?: boolean;
logErrors?: boolean;
}): Observable<T>;
/**
* Get detailed information about a specific subscription
*/
getSubscriptionInfo(id: string): SubscriptionDebugInfo | undefined;
/**
* Get all active subscriptions
*/
getActiveSubscriptions(): SubscriptionDebugInfo[];
/**
* Get all subscriptions (active and completed)
*/
getAllSubscriptions(): SubscriptionDebugInfo[];
/**
* Get performance metrics
*/
getPerformanceMetrics(): PerformanceMetrics;
/**
* Log current subscription status
*/
logStatus(): void;
/**
* Detect potential memory leaks
*/
detectPotentialLeaks(): number;
/**
* Clear all debug information
*/
clear(): void;
/**
* Export debug information as JSON
*/
exportDebugInfo(): string;
private captureStackTrace;
private calculateAverageLifetime;
private getCurrentMemoryUsage;
private setupPerformanceMonitoring;
private setupMemoryMonitoring;
ngOnDestroy(): void;
static ɵfac: i0.ɵɵFactoryDeclaration<SubscriptionDebuggerService, never>;
static ɵprov: i0.ɵɵInjectableDeclaration<SubscriptionDebuggerService>;
}
/**
* Mock subscription manager for testing
*/
declare class MockSubscriptionManager {
private subscriptions;
private subscriptionCount;
add(subscription: Subscription, name?: string): string;
remove(id: string): boolean;
removeAll(): void;
getActiveCount(): number;
getAllCount(): number;
isActive(id: string): boolean;
}
/**
* Test observable that can be controlled for testing subscription behavior
*/
declare class TestObservable<T> extends Observable<T> {
private subject;
private isCompleted;
private hasErrored;
constructor();
/**
* Emit a value to all subscribers
*/
emit(value: T): void;
/**
* Emit multiple values with optional delays
*/
emitSequence(values: T[], delayMs?: number): Promise<void>;
/**
* Complete the observable
*/
complete(): void;
/**
* Emit an error
*/
error(error: any): void;
/**
* Get the current state
*/
getState(): {
completed: boolean;
errored: boolean;
};
}
/**
* Subscription testing utilities
*/
declare class SubscriptionTester {
private subscriptions;
private emissionCounts;
private errorCounts;
private completionCounts;
/**
* Subscribe to an observable with tracking
*/
subscribe<T>(observable: Observable<T>, name: string, options?: {
onNext?: (value: T) => void;
onError?: (error: any) => void;
onComplete?: () => void;
}): Subscription;
/**
* Get emission count for a named subscription
*/
getEmissionCount(name: string): number;
/**
* Get error count for a named subscription
*/
getErrorCount(name: string): number;
/**
* Get completion count for a named subscription
*/
getCompletionCount(name: string): number;
/**
* Get total active subscriptions
*/
getActiveSubscriptionCount(): number;
/**
* Unsubscribe from all tracked subscriptions
*/
unsubscribeAll(): void;
/**
* Reset all counters
*/
reset(): void;
/**
* Wait for a specific number of emissions
*/
waitForEmissions(name: string, count: number, timeoutMs?: number): Promise<void>;
/**
* Wait for completion
*/
waitForCompletion(name: string, timeoutMs?: number): Promise<void>;
private incrementCount;
}
/**
* Memory leak detector for testing
*/
declare class MemoryLeakDetector {
private initialMemory?;
private subscriptions;
/**
* Start monitoring memory usage
*/
startMonitoring(): void;
/**
* Add subscription to monitor
*/
track(subscription: Subscription): void;
/**
* Check for memory leaks
*/
checkForLeaks(): {
hasLeaks: boolean;
memoryIncrease: number;
activeSubscriptions: number;
totalSubscriptions: number;
};
/**
* Clean up all tracked subscriptions
*/
cleanup(): void;
}
/**
* Helper functions for creating test scenarios
*/
declare const TestScenarios: {
/**
* Create an observable that emits values at intervals
*/
createIntervalObservable(intervalMs: number, maxValues?: number): Observable<number>;
/**
* Create an observable that errors after a delay
*/
createErrorObservable(delayMs: number, error?: any): Observable<never>;
/**
* Create an observable that never completes or errors
*/
createInfiniteObservable(): Observable<number>;
/**
* Create an observable that completes immediately
*/
createImmediateCompletionObservable(): Observable<never>;
/**
* Create an observable that emits once then completes
*/
createSingleValueObservable<T>(value: T, delayMs?: number): Observable<T>;
};
/**
* RxJS operator that automatically unsubscribes from form value changes when component is destroyed
*/
declare function takeUntilFormDestroyed(): <T>(source: Observable<T>) => Observable<T>;
/**
* RxJS operator that emits only when form is valid
*/
declare function takeWhileFormValid<T>(isValid: () => boolean): (source: Observable<T>) => Observable<T>;
/**
* Injectable service for managing form subscriptions
*/
declare class FormSubscriptionManager {
private subscriptions;
/**
* Create a managed subscription with automatic cleanup
*/
manage<T>(source: Observable<T>, name: string): Observable<T>;
/**
* Unsubscribe from a specific managed subscription
*/
unsubscribe(name: string): void;
/**
* Unsubscribe from all managed subscriptions
*/
unsubscribeAll(): void;
/**
* Get count of active subscriptions
*/
getActiveCount(): number;
static ɵfac: i0.ɵɵFactoryDeclaration<FormSubscriptionManager, never>;
static ɵprov: i0.ɵɵInjectableDeclaration<FormSubscriptionManager>;
}
/**
* Memory usage statistics
*/
interface MemoryStats {
totalObservables: number;
activeSubscriptions: number;
sharedObservables: number;
memoryUsage: number;
potentialLeaks: number;
}
/**
* Memory optimizer for RxJS observables
*/
declare class MemoryOptimizer {
private static instance;
private observables;
private sharedObservables;
private idCounter;
private isEnabled;
static getInstance(): MemoryOptimizer;
/**
* Enable memory optimization tracking
*/
enable(): void;
/**
* Disable memory optimization tracking
*/
disable(): void;
/**
* Create a memory-optimized observable with automatic sharing
*/
optimize<T>(source: Observable<T>, options?: {
share?: boolean;
shareReplay?: number;
name?: string;
}): Observable<T>;
/**
* Create a shared observable that automatically cleans up when no subscribers
*/
shareWithCleanup<T>(source: Observable<T>, cleanupDelayMs?: number): Observable<T>;
/**
* Create a memory-efficient BehaviorSubject
*/
createEfficientBehaviorSubject<T>(initialValue: T, maxHistorySize?: number): BehaviorSubject<T>;
/**
* Create a memory-efficient ReplaySubject
*/
createEfficientReplaySubject<T>(bufferSize?: number, windowTime?: number): ReplaySubject<T>;
/**
* Get memory usage statistics
*/
getMemoryStats(): MemoryStats;
/**
* Clean up stale observables
*/
cleanup(maxAge?: number): number;
/**
* Force garbage collection (if available)
*/
forceGarbageCollection(): void;
private estimateMemoryUsage;
private getCurrentMemoryUsage;
}
/**
* RxJS operator for memory optimization
*/
declare function optimizeMemory<T>(options?: {
share?: boolean;
shareReplay?: number;
name?: string;
}): (source: Observable<T>) => Observable<T>;
/**
* RxJS operator that automatically shares observables with cleanup
*/
declare function shareWithAutoCleanup<T>(cleanupDelayMs?: number): (source: Observable<T>) => Observable<T>;
/**
* RxJS operator that limits emission frequency to reduce memory pressure
*/
declare function limitEmissionRate<T>(maxEmissionsPerSecond?: number): (source: Observable<T>) => Observable<T>;
/**
* Utility functions for memory management
*/
declare const MemoryUtils: {
/**
* Get current memory usage (if available)
*/
getCurrentMemoryUsage(): {
used: number;
total: number;
limit: number;
} | null;
/**
* Check if memory usage is high
*/
isMemoryUsageHigh(): boolean;
/**
* Log memory statistics
*/
logMemoryStats(): void;
};
export { FormSubscriptionManager, HttpRequestManager, MemoryLeakDetector, MemoryOptimizer, MemoryUtils, MockSubscriptionManager, NgTerminusModule, SubscriptionDebugger, SubscriptionDebuggerService, SubscriptionManager, SubscriptionTester, TestObservable, TestScenarios, bufferWhileHidden, cancelOnDestroy, cancelPrevious, createManagedObservable, isObservableValue, isSubscription, limitEmissionRate, logHttpRequests, manageManyObservables, optimizeMemory, retryWithBackoff, safeUnsubscribe, shareWithAutoCleanup, takeUntilDestroyed, takeUntilFormDestroyed, takeUntilHidden, takeUntilRoute, takeWhileFormValid, takeWhileOnRoute, takeWhileVisible, throttleWhileHidden, untilDestroyed };
export type { CleanupFunction, MemoryStats, NgTerminusConfig, ObservableFactory, PerformanceMetrics, SubscriptionConfig, SubscriptionDebugInfo, SubscriptionDebugOptions, TakeUntilDestroyed };