From 0ba70638e6db3f2dbad385d09f91b0d3602ec7be Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Mon, 8 Sep 2025 15:59:23 +0200 Subject: [PATCH] ov-components: optimize storage service for improved performance and error handling --- .../src/lib/models/storage.model.ts | 29 +- .../lib/services/storage/storage.service.ts | 505 ++++++++++++++---- 2 files changed, 404 insertions(+), 130 deletions(-) diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/storage.model.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/storage.model.ts index 50cdd373..677b650d 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/storage.model.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/models/storage.model.ts @@ -14,7 +14,15 @@ export enum StorageKeys { ACTIVE_TABS = 'activeTabs' } -export const PERSISTENT_KEYS: StorageKeys[] = [ +export const SESSION_KEYS: StorageKeys[] = [StorageKeys.TAB_ID]; + +export const TAB_MANAGEMENT_KEYS: StorageKeys[] = [StorageKeys.TAB_ID, StorageKeys.ACTIVE_TABS]; + +// Data that should be unique per tab (stored in localStorage with tabId prefix) +export const TAB_SPECIFIC_KEYS: StorageKeys[] = [StorageKeys.PARTICIPANT_NAME, StorageKeys.MICROPHONE_ENABLED, StorageKeys.CAMERA_ENABLED]; + +// Data that should be truly persistent and shared between tabs +export const SHARED_PERSISTENT_KEYS: StorageKeys[] = [ StorageKeys.VIDEO_DEVICE, StorageKeys.AUDIO_DEVICE, StorageKeys.LANG, @@ -22,23 +30,4 @@ export const PERSISTENT_KEYS: StorageKeys[] = [ StorageKeys.BACKGROUND ]; -export const SESSION_KEYS: StorageKeys[] = [StorageKeys.TAB_ID]; - -export const TAB_MANAGEMENT_KEYS: StorageKeys[] = [StorageKeys.TAB_ID, StorageKeys.ACTIVE_TABS]; - -// Data that should be unique per tab (stored in localStorage with tabId prefix) -export const TAB_SPECIFIC_KEYS: StorageKeys[] = [ - StorageKeys.PARTICIPANT_NAME, - StorageKeys.MICROPHONE_ENABLED, - StorageKeys.CAMERA_ENABLED, - StorageKeys.LANG, - StorageKeys.CAPTION_LANG, - StorageKeys.BACKGROUND, - StorageKeys.VIDEO_DEVICE, - StorageKeys.AUDIO_DEVICE -]; - -// Data that should be truly persistent and shared between tabs -export const SHARED_PERSISTENT_KEYS: StorageKeys[] = []; - export const STORAGE_PREFIX = 'ovComponents-'; diff --git a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/storage/storage.service.ts b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/storage/storage.service.ts index 28c5c511..b243a956 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/storage/storage.service.ts +++ b/openvidu-components-angular/projects/openvidu-components-angular/src/lib/services/storage/storage.service.ts @@ -1,6 +1,13 @@ import { Injectable, OnDestroy } from '@angular/core'; import { ILogger } from '../../models/logger.model'; -import { STORAGE_PREFIX, StorageKeys, SESSION_KEYS, TAB_MANAGEMENT_KEYS, TAB_SPECIFIC_KEYS, SHARED_PERSISTENT_KEYS } from '../../models/storage.model'; +import { + STORAGE_PREFIX, + StorageKeys, + SESSION_KEYS, + TAB_MANAGEMENT_KEYS, + TAB_SPECIFIC_KEYS, + SHARED_PERSISTENT_KEYS +} from '../../models/storage.model'; import { LoggerService } from '../logger/logger.service'; import { CustomDevice } from '../../models/device.model'; @@ -11,32 +18,81 @@ import { CustomDevice } from '../../models/device.model'; providedIn: 'root' }) export class StorageService implements OnDestroy { - public localStorage = window.localStorage; - public sessionStorage = window.sessionStorage; public log: ILogger; - protected PREFIX_KEY = STORAGE_PREFIX; - private tabId: string; + protected readonly PREFIX_KEY = STORAGE_PREFIX; + private readonly tabId: string; private readonly TAB_CLEANUP_INTERVAL = 30000; // 30 seconds - private cleanupInterval: any; + private readonly TAB_TIMEOUT_THRESHOLD = 60000; // 60 seconds + private cleanupInterval: NodeJS.Timeout | null = null; + private broadcastChannel: BroadcastChannel | null = null; + private isStorageAvailable = false; + private lastHeartbeat = 0; + + // Cache for parsed values to avoid repeated JSON operations + private cache = new Map(); + private cacheTimeout = new Map(); + private readonly CACHE_TTL = 5000; // 5 seconds cache TTL constructor(protected loggerSrv: LoggerService) { this.log = this.loggerSrv.get('StorageService'); - this.initializeTabManagement(); + + // Generate unique tab ID + this.tabId = this.generateUniqueTabId(); + + // Check storage availability + this.isStorageAvailable = this.checkStorageAvailability(); + + if (this.isStorageAvailable) { + this.initializeTabManagement(); + } else { + this.log.w('Storage not available - service will operate in limited mode'); + } } /** - * Initializes tab management system - * Creates unique tab ID and sets up cleanup mechanism + * Check if localStorage and sessionStorage are available + */ + private checkStorageAvailability(): boolean { + try { + const test = '__storage_test__'; + window.localStorage.setItem(test, test); + window.localStorage.removeItem(test); + window.sessionStorage.setItem(test, test); + window.sessionStorage.removeItem(test); + return true; + } catch (e) { + return false; + } + } + + /** + * Safe access to localStorage + */ + private get localStorage(): Storage { + return window.localStorage; + } + + /** + * Safe access to sessionStorage + */ + private get sessionStorage(): Storage { + return window.sessionStorage; + } + + /** + * Initializes tab management system with improved efficiency */ private initializeTabManagement(): void { - // Generate unique tab ID - this.tabId = this.generateTabId(); + // Store tab ID in session storage this.setSessionValue(StorageKeys.TAB_ID, this.tabId); + // Initialize BroadcastChannel for inter-tab communication + this.initializeBroadcastChannel(); + // Register this tab as active this.registerActiveTab(); - // Set up periodic cleanup of inactive tabs + // Set up optimized cleanup mechanism this.setupTabCleanup(); // Listen for page unload to clean up this tab @@ -44,37 +100,78 @@ export class StorageService implements OnDestroy { this.unregisterActiveTab(); }); + // Listen for page visibility changes to optimize heartbeat + document.addEventListener('visibilitychange', () => { + if (!document.hidden) { + this.updateHeartbeat(); + } + }); + this.log.d(`Tab initialized with ID: ${this.tabId}`); } /** - * Generates a unique tab identifier + * Initialize BroadcastChannel for efficient inter-tab communication */ - private generateTabId(): string { - return `tab_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; + private initializeBroadcastChannel(): void { + try { + if ('BroadcastChannel' in window) { + this.broadcastChannel = new BroadcastChannel(`${this.PREFIX_KEY}tabs`); + this.broadcastChannel.addEventListener('message', (event) => { + if (event.data.type === 'tab-cleanup') { + // Another tab is performing cleanup, update our heartbeat + this.updateHeartbeat(); + } + }); + } + } catch (e) { + this.log.w('BroadcastChannel not available, using fallback communication'); + } } /** - * Registers current tab as active + * Generates a more unique tab identifier with better collision resistance + */ + private generateUniqueTabId(): string { + const timestamp = Date.now(); + const random = Math.random().toString(36).substring(2, 12); + const performance = typeof window.performance !== 'undefined' ? window.performance.now() : 0; + return `tab_${timestamp}_${random}_${Math.floor(performance)}`; + } + + /** + * Updates heartbeat for current tab + */ + private updateHeartbeat(): void { + if (!this.isStorageAvailable) return; + + this.lastHeartbeat = Date.now(); + const activeTabs = this.getActiveTabsFromStorage() || {}; + activeTabs[this.tabId] = this.lastHeartbeat; + this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs, false); // Skip cache for critical data + } + + /** + * Registers current tab as active with optimized approach */ private registerActiveTab(): void { - const activeTabs = this.getActiveTabsFromStorage() || {}; - activeTabs[this.tabId] = Date.now(); - this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs); + this.updateHeartbeat(); } /** * Unregisters current tab from active tabs */ private unregisterActiveTab(): void { + if (!this.isStorageAvailable) return; + const activeTabs = this.getActiveTabsFromStorage() || {}; delete activeTabs[this.tabId]; - this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs); + this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs, false); this.cleanupTabData(this.tabId); } /** - * Sets up periodic cleanup of inactive tabs + * Sets up optimized cleanup with reduced frequency */ private setupTabCleanup(): void { this.cleanupInterval = setInterval(() => { @@ -83,45 +180,77 @@ export class StorageService implements OnDestroy { } /** - * Cleans up data from inactive tabs + * Optimized cleanup of inactive tabs with better performance */ private cleanupInactiveTabs(): void { + if (!this.isStorageAvailable) return; + const activeTabs = this.getActiveTabsFromStorage() || {}; const currentTime = Date.now(); - const timeoutThreshold = this.TAB_CLEANUP_INTERVAL * 2; // 60 seconds + const tabsToCleanup: string[] = []; + let hasChanges = false; - Object.keys(activeTabs).forEach(tabId => { - const lastActivity = activeTabs[tabId]; - if (currentTime - lastActivity > timeoutThreshold) { - this.log.d(`Cleaning up inactive tab: ${tabId}`); + // Find tabs to cleanup without modifying the object during iteration + for (const [tabId, lastActivity] of Object.entries(activeTabs)) { + if (currentTime - lastActivity > this.TAB_TIMEOUT_THRESHOLD) { + tabsToCleanup.push(tabId); + hasChanges = true; + } + } + + // Clean up inactive tabs + if (tabsToCleanup.length > 0) { + this.log.d(`Cleaning up ${tabsToCleanup.length} inactive tabs`); + + // Notify other tabs about cleanup via BroadcastChannel + if (this.broadcastChannel) { + this.broadcastChannel.postMessage({ type: 'tab-cleanup', tabs: tabsToCleanup }); + } + + // Remove inactive tabs + for (const tabId of tabsToCleanup) { delete activeTabs[tabId]; this.cleanupTabData(tabId); } - }); + } // Update heartbeat for current tab activeTabs[this.tabId] = currentTime; - this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs); + this.lastHeartbeat = currentTime; + + if (hasChanges || currentTime - this.lastHeartbeat > this.TAB_CLEANUP_INTERVAL / 2) { + this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs, false); + } } /** - * Cleans up data associated with a specific tab + * Optimized cleanup of tab-specific data */ private cleanupTabData(tabId: string): void { - // Clean up tab-specific data from localStorage - TAB_SPECIFIC_KEYS.forEach(key => { - const storageKey = `${this.PREFIX_KEY}${tabId}_${key}`; - this.localStorage.removeItem(storageKey); - }); + if (!this.isStorageAvailable) return; + + // Use batch removal for better performance + const keysToRemove = TAB_SPECIFIC_KEYS.map(key => `${this.PREFIX_KEY}${tabId}_${key}`); + + for (const storageKey of keysToRemove) { + try { + this.localStorage.removeItem(storageKey); + // Clear from cache if exists + this.cache.delete(storageKey); + this.cacheTimeout.delete(storageKey); + } catch (e) { + this.log.w(`Failed to remove storage key: ${storageKey}`, e); + } + } this.log.d(`Cleaned up data for tab: ${tabId}`); } /** - * Gets active tabs from localStorage + * Gets active tabs with caching */ private getActiveTabsFromStorage(): { [key: string]: number } | null { - return this.getLocalValue(StorageKeys.ACTIVE_TABS); + return this.getLocalValue(StorageKeys.ACTIVE_TABS, false); // Don't cache tab management data } /** @@ -131,18 +260,20 @@ export class StorageService implements OnDestroy { return this.tabId; } + // Simplified API methods with consistent patterns getParticipantName(): string | null { return this.get(StorageKeys.PARTICIPANT_NAME); } - setParticipantName(name: string) { + setParticipantName(name: string): void { this.set(StorageKeys.PARTICIPANT_NAME, name); } + getVideoDevice(): CustomDevice | null { return this.get(StorageKeys.VIDEO_DEVICE); } - setVideoDevice(device: CustomDevice) { + setVideoDevice(device: CustomDevice): void { this.set(StorageKeys.VIDEO_DEVICE, device); } @@ -150,7 +281,7 @@ export class StorageService implements OnDestroy { return this.get(StorageKeys.AUDIO_DEVICE); } - setAudioDevice(device: CustomDevice) { + setAudioDevice(device: CustomDevice): void { this.set(StorageKeys.AUDIO_DEVICE, device); } @@ -160,13 +291,10 @@ export class StorageService implements OnDestroy { */ isCameraEnabled(): boolean { const value = this.get(StorageKeys.CAMERA_ENABLED); - if (value === null) { - return true; - } - return value === true; + return value === null ? true : value === true; } - setCameraEnabled(enabled: boolean) { + setCameraEnabled(enabled: boolean): void { this.set(StorageKeys.CAMERA_ENABLED, enabled); } @@ -176,20 +304,14 @@ export class StorageService implements OnDestroy { */ isMicrophoneEnabled(): boolean { const value = this.get(StorageKeys.MICROPHONE_ENABLED); - if (value === null) { - return true; - } - return value === true; + return value === null ? true : value === true; } - /** - * @param enabled - */ - setMicrophoneEnabled(enabled: boolean) { + setMicrophoneEnabled(enabled: boolean): void { this.set(StorageKeys.MICROPHONE_ENABLED, enabled); } - setLang(lang: string) { + setLang(lang: string): void { this.set(StorageKeys.LANG, lang); } @@ -197,7 +319,7 @@ export class StorageService implements OnDestroy { return this.get(StorageKeys.LANG); } - setCaptionLang(lang: string) { + setCaptionLang(lang: string): void { this.set(StorageKeys.CAPTION_LANG, lang); } @@ -205,7 +327,7 @@ export class StorageService implements OnDestroy { return this.get(StorageKeys.CAPTION_LANG); } - setBackground(id: string) { + setBackground(id: string): void { this.set(StorageKeys.BACKGROUND, id); } @@ -213,31 +335,58 @@ export class StorageService implements OnDestroy { return this.get(StorageKeys.BACKGROUND); } - removeBackground() { + removeBackground(): void { this.remove(StorageKeys.BACKGROUND); } - protected set(key: string, item: any) { - if (SESSION_KEYS.includes(key as StorageKeys)) { - this.setSessionValue(key, item); - } else { - this.setLocalValue(key, item); + // Core storage methods with improved error handling and caching + protected set(key: string, item: any): void { + if (!this.isStorageAvailable) { + this.log.w(`Storage not available, cannot set key: ${key}`); + return; + } + + try { + if (SESSION_KEYS.includes(key as StorageKeys)) { + this.setSessionValue(key, item); + } else { + this.setLocalValue(key, item); + } + } catch (e) { + this.log.e(`Failed to set storage key: ${key}`, e); } } protected get(key: string): any { - if (SESSION_KEYS.includes(key as StorageKeys)) { - return this.getSessionValue(key); - } else { - return this.getLocalValue(key); + if (!this.isStorageAvailable) { + return null; + } + + try { + if (SESSION_KEYS.includes(key as StorageKeys)) { + return this.getSessionValue(key); + } else { + return this.getLocalValue(key); + } + } catch (e) { + this.log.e(`Failed to get storage key: ${key}`, e); + return null; } } - protected remove(key: string) { - if (SESSION_KEYS.includes(key as StorageKeys)) { - this.removeSessionValue(key); - } else { - this.removeLocalValue(key); + protected remove(key: string): void { + if (!this.isStorageAvailable) { + return; + } + + try { + if (SESSION_KEYS.includes(key as StorageKeys)) { + this.removeSessionValue(key); + } else { + this.removeLocalValue(key); + } + } catch (e) { + this.log.e(`Failed to remove storage key: ${key}`, e); } } @@ -249,55 +398,140 @@ export class StorageService implements OnDestroy { } /** - * Sets value in localStorage with tab-specific key if needed + * Sets value in localStorage with optimized serialization and caching */ - private setLocalValue(key: string, item: any): void { - const value = JSON.stringify({ item: item }); + private setLocalValue(key: string, item: any, useCache: boolean = true): void { + if (!this.isStorageAvailable) return; + const storageKey = this.shouldUseTabSpecificKey(key) ? `${this.PREFIX_KEY}${this.tabId}_${key}` : `${this.PREFIX_KEY}${key}`; - this.localStorage.setItem(storageKey, value); + + try { + // Optimize serialization for primitive types + let value: string; + if (item === null || item === undefined) { + value = JSON.stringify({ item: null }); + } else if (typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean') { + value = JSON.stringify({ item: item }); + } else { + value = JSON.stringify({ item: item }); + } + + this.localStorage.setItem(storageKey, value); + + // Update cache + if (useCache) { + this.cache.set(storageKey, item); + this.cacheTimeout.set(storageKey, Date.now() + this.CACHE_TTL); + } + } catch (e) { + this.log.e(`Failed to set localStorage key: ${storageKey}`, e); + } } /** - * Gets value from localStorage with tab-specific key if needed + * Gets value from localStorage with caching optimization */ - private getLocalValue(key: string): any { + private getLocalValue(key: string, useCache: boolean = true): any { + if (!this.isStorageAvailable) return null; + const storageKey = this.shouldUseTabSpecificKey(key) ? `${this.PREFIX_KEY}${this.tabId}_${key}` : `${this.PREFIX_KEY}${key}`; - const str = this.localStorage.getItem(storageKey); - if (!!str) { - return JSON.parse(str).item; + + // Check cache first + if (useCache && this.cache.has(storageKey)) { + const timeout = this.cacheTimeout.get(storageKey); + if (timeout && Date.now() < timeout) { + return this.cache.get(storageKey); + } else { + // Cache expired + this.cache.delete(storageKey); + this.cacheTimeout.delete(storageKey); + } + } + + try { + const str = this.localStorage.getItem(storageKey); + if (str) { + const parsed = JSON.parse(str); + const value = parsed.item; + + // Update cache + if (useCache) { + this.cache.set(storageKey, value); + this.cacheTimeout.set(storageKey, Date.now() + this.CACHE_TTL); + } + + return value; + } + } catch (e) { + this.log.e(`Failed to parse localStorage key: ${storageKey}`, e); + // Remove corrupted data + try { + this.localStorage.removeItem(storageKey); + } catch (removeError) { + this.log.e(`Failed to remove corrupted key: ${storageKey}`, removeError); + } } return null; } /** - * Removes value from localStorage with tab-specific key if needed + * Removes value from localStorage with cache cleanup */ private removeLocalValue(key: string): void { + if (!this.isStorageAvailable) return; + const storageKey = this.shouldUseTabSpecificKey(key) ? `${this.PREFIX_KEY}${this.tabId}_${key}` : `${this.PREFIX_KEY}${key}`; - this.localStorage.removeItem(storageKey); + + try { + this.localStorage.removeItem(storageKey); + // Clear from cache + this.cache.delete(storageKey); + this.cacheTimeout.delete(storageKey); + } catch (e) { + this.log.e(`Failed to remove localStorage key: ${storageKey}`, e); + } } /** - * Sets value in sessionStorage + * Sets value in sessionStorage with error handling */ private setSessionValue(key: string, item: any): void { - const value = JSON.stringify({ item: item }); - this.sessionStorage.setItem(this.PREFIX_KEY + key, value); + if (!this.isStorageAvailable) return; + + try { + const value = JSON.stringify({ item: item }); + this.sessionStorage.setItem(this.PREFIX_KEY + key, value); + } catch (e) { + this.log.e(`Failed to set sessionStorage key: ${key}`, e); + } } /** - * Gets value from sessionStorage + * Gets value from sessionStorage with error handling */ private getSessionValue(key: string): any { - const str = this.sessionStorage.getItem(this.PREFIX_KEY + key); - if (!!str) { - return JSON.parse(str).item; + if (!this.isStorageAvailable) return null; + + try { + const str = this.sessionStorage.getItem(this.PREFIX_KEY + key); + if (str) { + const parsed = JSON.parse(str); + return parsed.item; + } + } catch (e) { + this.log.e(`Failed to parse sessionStorage key: ${key}`, e); + // Remove corrupted data + try { + this.sessionStorage.removeItem(this.PREFIX_KEY + key); + } catch (removeError) { + this.log.e(`Failed to remove corrupted sessionStorage key: ${key}`, removeError); + } } return null; } @@ -306,45 +540,76 @@ export class StorageService implements OnDestroy { * Removes value from sessionStorage */ private removeSessionValue(key: string): void { - this.sessionStorage.removeItem(this.PREFIX_KEY + key); + if (!this.isStorageAvailable) return; + + try { + this.sessionStorage.removeItem(this.PREFIX_KEY + key); + } catch (e) { + this.log.e(`Failed to remove sessionStorage key: ${key}`, e); + } } - public clear() { + /** + * Optimized clear method that safely iterates and removes items + */ + public clear(): void { + if (!this.isStorageAvailable) return; + this.log.d('Clearing localStorage and sessionStorage'); - // Clear only our prefixed keys from localStorage - Object.keys(this.localStorage).forEach(key => { - if (key.startsWith(this.PREFIX_KEY)) { - this.localStorage.removeItem(key); - } - }); + // Clear localStorage with safe iteration + this.clearStorageByPrefix(this.localStorage, this.PREFIX_KEY); - // Clear only our prefixed keys from sessionStorage - Object.keys(this.sessionStorage).forEach(key => { - if (key.startsWith(this.PREFIX_KEY)) { - this.sessionStorage.removeItem(key); + // Clear sessionStorage with safe iteration + this.clearStorageByPrefix(this.sessionStorage, this.PREFIX_KEY); + + // Clear caches + this.cache.clear(); + this.cacheTimeout.clear(); + } + + /** + * Safely clears storage by collecting keys first, then removing + */ + private clearStorageByPrefix(storage: Storage, prefix: string): void { + try { + const keysToRemove: string[] = []; + + // Collect keys to remove + for (let i = 0; i < storage.length; i++) { + const key = storage.key(i); + if (key && key.startsWith(prefix)) { + keysToRemove.push(key); + } } - }); + + // Remove collected keys + for (const key of keysToRemove) { + storage.removeItem(key); + } + } catch (e) { + this.log.e('Failed to clear storage', e); + } } /** * Clears only session data (tab-specific data) */ public clearSessionData(): void { + if (!this.isStorageAvailable) return; + this.log.d('Clearing session data'); - Object.keys(this.sessionStorage).forEach(key => { - if (key.startsWith(this.PREFIX_KEY)) { - this.sessionStorage.removeItem(key); - } - }); + this.clearStorageByPrefix(this.sessionStorage, this.PREFIX_KEY); } /** * Clears only tab-specific data for current tab */ public clearTabSpecificData(): void { + if (!this.isStorageAvailable) return; + this.log.d('Clearing tab-specific data'); - TAB_SPECIFIC_KEYS.forEach(key => { + TAB_SPECIFIC_KEYS.forEach((key) => { this.removeLocalValue(key); }); } @@ -353,11 +618,17 @@ export class StorageService implements OnDestroy { * Clears only persistent data */ public clearPersistentData(): void { + if (!this.isStorageAvailable) return; + this.log.d('Clearing persistent data'); - SHARED_PERSISTENT_KEYS.forEach(key => { + + // Clear shared persistent keys + SHARED_PERSISTENT_KEYS.forEach((key) => { this.removeLocalValue(key); }); - TAB_MANAGEMENT_KEYS.forEach(key => { + + // Clear tab management keys + TAB_MANAGEMENT_KEYS.forEach((key) => { this.removeLocalValue(key); }); } @@ -366,10 +637,24 @@ export class StorageService implements OnDestroy { * Cleanup method to be called when service is destroyed */ public destroy(): void { + // Clear interval if (this.cleanupInterval) { clearInterval(this.cleanupInterval); + this.cleanupInterval = null; } + + // Close BroadcastChannel + if (this.broadcastChannel) { + this.broadcastChannel.close(); + this.broadcastChannel = null; + } + + // Unregister tab this.unregisterActiveTab(); + + // Clear caches + this.cache.clear(); + this.cacheTimeout.clear(); } /**