ov-components: optimize storage service for improved performance and error handling

master
Carlos Santos 2025-09-08 15:59:23 +02:00
parent f6d1b6e86c
commit 0ba70638e6
2 changed files with 404 additions and 130 deletions

View File

@ -14,7 +14,15 @@ export enum StorageKeys {
ACTIVE_TABS = 'activeTabs' 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.VIDEO_DEVICE,
StorageKeys.AUDIO_DEVICE, StorageKeys.AUDIO_DEVICE,
StorageKeys.LANG, StorageKeys.LANG,
@ -22,23 +30,4 @@ export const PERSISTENT_KEYS: StorageKeys[] = [
StorageKeys.BACKGROUND 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-'; export const STORAGE_PREFIX = 'ovComponents-';

View File

@ -1,6 +1,13 @@
import { Injectable, OnDestroy } from '@angular/core'; import { Injectable, OnDestroy } from '@angular/core';
import { ILogger } from '../../models/logger.model'; 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 { LoggerService } from '../logger/logger.service';
import { CustomDevice } from '../../models/device.model'; import { CustomDevice } from '../../models/device.model';
@ -11,32 +18,81 @@ import { CustomDevice } from '../../models/device.model';
providedIn: 'root' providedIn: 'root'
}) })
export class StorageService implements OnDestroy { export class StorageService implements OnDestroy {
public localStorage = window.localStorage;
public sessionStorage = window.sessionStorage;
public log: ILogger; public log: ILogger;
protected PREFIX_KEY = STORAGE_PREFIX; protected readonly PREFIX_KEY = STORAGE_PREFIX;
private tabId: string; private readonly tabId: string;
private readonly TAB_CLEANUP_INTERVAL = 30000; // 30 seconds 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<string, any>();
private cacheTimeout = new Map<string, number>();
private readonly CACHE_TTL = 5000; // 5 seconds cache TTL
constructor(protected loggerSrv: LoggerService) { constructor(protected loggerSrv: LoggerService) {
this.log = this.loggerSrv.get('StorageService'); 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 * Check if localStorage and sessionStorage are available
* Creates unique tab ID and sets up cleanup mechanism */
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 { private initializeTabManagement(): void {
// Generate unique tab ID // Store tab ID in session storage
this.tabId = this.generateTabId();
this.setSessionValue(StorageKeys.TAB_ID, this.tabId); this.setSessionValue(StorageKeys.TAB_ID, this.tabId);
// Initialize BroadcastChannel for inter-tab communication
this.initializeBroadcastChannel();
// Register this tab as active // Register this tab as active
this.registerActiveTab(); this.registerActiveTab();
// Set up periodic cleanup of inactive tabs // Set up optimized cleanup mechanism
this.setupTabCleanup(); this.setupTabCleanup();
// Listen for page unload to clean up this tab // Listen for page unload to clean up this tab
@ -44,37 +100,78 @@ export class StorageService implements OnDestroy {
this.unregisterActiveTab(); 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}`); this.log.d(`Tab initialized with ID: ${this.tabId}`);
} }
/** /**
* Generates a unique tab identifier * Initialize BroadcastChannel for efficient inter-tab communication
*/ */
private generateTabId(): string { private initializeBroadcastChannel(): void {
return `tab_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; 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 { private registerActiveTab(): void {
const activeTabs = this.getActiveTabsFromStorage() || {}; this.updateHeartbeat();
activeTabs[this.tabId] = Date.now();
this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs);
} }
/** /**
* Unregisters current tab from active tabs * Unregisters current tab from active tabs
*/ */
private unregisterActiveTab(): void { private unregisterActiveTab(): void {
if (!this.isStorageAvailable) return;
const activeTabs = this.getActiveTabsFromStorage() || {}; const activeTabs = this.getActiveTabsFromStorage() || {};
delete activeTabs[this.tabId]; delete activeTabs[this.tabId];
this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs); this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs, false);
this.cleanupTabData(this.tabId); this.cleanupTabData(this.tabId);
} }
/** /**
* Sets up periodic cleanup of inactive tabs * Sets up optimized cleanup with reduced frequency
*/ */
private setupTabCleanup(): void { private setupTabCleanup(): void {
this.cleanupInterval = setInterval(() => { 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 { private cleanupInactiveTabs(): void {
if (!this.isStorageAvailable) return;
const activeTabs = this.getActiveTabsFromStorage() || {}; const activeTabs = this.getActiveTabsFromStorage() || {};
const currentTime = Date.now(); const currentTime = Date.now();
const timeoutThreshold = this.TAB_CLEANUP_INTERVAL * 2; // 60 seconds const tabsToCleanup: string[] = [];
let hasChanges = false;
Object.keys(activeTabs).forEach(tabId => { // Find tabs to cleanup without modifying the object during iteration
const lastActivity = activeTabs[tabId]; for (const [tabId, lastActivity] of Object.entries(activeTabs)) {
if (currentTime - lastActivity > timeoutThreshold) { if (currentTime - lastActivity > this.TAB_TIMEOUT_THRESHOLD) {
this.log.d(`Cleaning up inactive tab: ${tabId}`); 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]; delete activeTabs[tabId];
this.cleanupTabData(tabId); this.cleanupTabData(tabId);
} }
}); }
// Update heartbeat for current tab // Update heartbeat for current tab
activeTabs[this.tabId] = currentTime; 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 { private cleanupTabData(tabId: string): void {
// Clean up tab-specific data from localStorage if (!this.isStorageAvailable) return;
TAB_SPECIFIC_KEYS.forEach(key => {
const storageKey = `${this.PREFIX_KEY}${tabId}_${key}`; // Use batch removal for better performance
this.localStorage.removeItem(storageKey); 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}`); 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 { 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; return this.tabId;
} }
// Simplified API methods with consistent patterns
getParticipantName(): string | null { getParticipantName(): string | null {
return this.get(StorageKeys.PARTICIPANT_NAME); return this.get(StorageKeys.PARTICIPANT_NAME);
} }
setParticipantName(name: string) { setParticipantName(name: string): void {
this.set(StorageKeys.PARTICIPANT_NAME, name); this.set(StorageKeys.PARTICIPANT_NAME, name);
} }
getVideoDevice(): CustomDevice | null { getVideoDevice(): CustomDevice | null {
return this.get(StorageKeys.VIDEO_DEVICE); return this.get(StorageKeys.VIDEO_DEVICE);
} }
setVideoDevice(device: CustomDevice) { setVideoDevice(device: CustomDevice): void {
this.set(StorageKeys.VIDEO_DEVICE, device); this.set(StorageKeys.VIDEO_DEVICE, device);
} }
@ -150,7 +281,7 @@ export class StorageService implements OnDestroy {
return this.get(StorageKeys.AUDIO_DEVICE); return this.get(StorageKeys.AUDIO_DEVICE);
} }
setAudioDevice(device: CustomDevice) { setAudioDevice(device: CustomDevice): void {
this.set(StorageKeys.AUDIO_DEVICE, device); this.set(StorageKeys.AUDIO_DEVICE, device);
} }
@ -160,13 +291,10 @@ export class StorageService implements OnDestroy {
*/ */
isCameraEnabled(): boolean { isCameraEnabled(): boolean {
const value = this.get(StorageKeys.CAMERA_ENABLED); const value = this.get(StorageKeys.CAMERA_ENABLED);
if (value === null) { return value === null ? true : value === true;
return true;
}
return value === true;
} }
setCameraEnabled(enabled: boolean) { setCameraEnabled(enabled: boolean): void {
this.set(StorageKeys.CAMERA_ENABLED, enabled); this.set(StorageKeys.CAMERA_ENABLED, enabled);
} }
@ -176,20 +304,14 @@ export class StorageService implements OnDestroy {
*/ */
isMicrophoneEnabled(): boolean { isMicrophoneEnabled(): boolean {
const value = this.get(StorageKeys.MICROPHONE_ENABLED); const value = this.get(StorageKeys.MICROPHONE_ENABLED);
if (value === null) { return value === null ? true : value === true;
return true;
}
return value === true;
} }
/** setMicrophoneEnabled(enabled: boolean): void {
* @param enabled
*/
setMicrophoneEnabled(enabled: boolean) {
this.set(StorageKeys.MICROPHONE_ENABLED, enabled); this.set(StorageKeys.MICROPHONE_ENABLED, enabled);
} }
setLang(lang: string) { setLang(lang: string): void {
this.set(StorageKeys.LANG, lang); this.set(StorageKeys.LANG, lang);
} }
@ -197,7 +319,7 @@ export class StorageService implements OnDestroy {
return this.get(StorageKeys.LANG); return this.get(StorageKeys.LANG);
} }
setCaptionLang(lang: string) { setCaptionLang(lang: string): void {
this.set(StorageKeys.CAPTION_LANG, lang); this.set(StorageKeys.CAPTION_LANG, lang);
} }
@ -205,7 +327,7 @@ export class StorageService implements OnDestroy {
return this.get(StorageKeys.CAPTION_LANG); return this.get(StorageKeys.CAPTION_LANG);
} }
setBackground(id: string) { setBackground(id: string): void {
this.set(StorageKeys.BACKGROUND, id); this.set(StorageKeys.BACKGROUND, id);
} }
@ -213,31 +335,58 @@ export class StorageService implements OnDestroy {
return this.get(StorageKeys.BACKGROUND); return this.get(StorageKeys.BACKGROUND);
} }
removeBackground() { removeBackground(): void {
this.remove(StorageKeys.BACKGROUND); this.remove(StorageKeys.BACKGROUND);
} }
protected set(key: string, item: any) { // Core storage methods with improved error handling and caching
if (SESSION_KEYS.includes(key as StorageKeys)) { protected set(key: string, item: any): void {
this.setSessionValue(key, item); if (!this.isStorageAvailable) {
} else { this.log.w(`Storage not available, cannot set key: ${key}`);
this.setLocalValue(key, item); 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 { protected get(key: string): any {
if (SESSION_KEYS.includes(key as StorageKeys)) { if (!this.isStorageAvailable) {
return this.getSessionValue(key); return null;
} else { }
return this.getLocalValue(key);
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) { protected remove(key: string): void {
if (SESSION_KEYS.includes(key as StorageKeys)) { if (!this.isStorageAvailable) {
this.removeSessionValue(key); return;
} else { }
this.removeLocalValue(key);
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 { private setLocalValue(key: string, item: any, useCache: boolean = true): void {
const value = JSON.stringify({ item: item }); if (!this.isStorageAvailable) return;
const storageKey = this.shouldUseTabSpecificKey(key) const storageKey = this.shouldUseTabSpecificKey(key)
? `${this.PREFIX_KEY}${this.tabId}_${key}` ? `${this.PREFIX_KEY}${this.tabId}_${key}`
: `${this.PREFIX_KEY}${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) const storageKey = this.shouldUseTabSpecificKey(key)
? `${this.PREFIX_KEY}${this.tabId}_${key}` ? `${this.PREFIX_KEY}${this.tabId}_${key}`
: `${this.PREFIX_KEY}${key}`; : `${this.PREFIX_KEY}${key}`;
const str = this.localStorage.getItem(storageKey);
if (!!str) { // Check cache first
return JSON.parse(str).item; 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; return null;
} }
/** /**
* Removes value from localStorage with tab-specific key if needed * Removes value from localStorage with cache cleanup
*/ */
private removeLocalValue(key: string): void { private removeLocalValue(key: string): void {
if (!this.isStorageAvailable) return;
const storageKey = this.shouldUseTabSpecificKey(key) const storageKey = this.shouldUseTabSpecificKey(key)
? `${this.PREFIX_KEY}${this.tabId}_${key}` ? `${this.PREFIX_KEY}${this.tabId}_${key}`
: `${this.PREFIX_KEY}${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 { private setSessionValue(key: string, item: any): void {
const value = JSON.stringify({ item: item }); if (!this.isStorageAvailable) return;
this.sessionStorage.setItem(this.PREFIX_KEY + key, value);
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 { private getSessionValue(key: string): any {
const str = this.sessionStorage.getItem(this.PREFIX_KEY + key); if (!this.isStorageAvailable) return null;
if (!!str) {
return JSON.parse(str).item; 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; return null;
} }
@ -306,45 +540,76 @@ export class StorageService implements OnDestroy {
* Removes value from sessionStorage * Removes value from sessionStorage
*/ */
private removeSessionValue(key: string): void { 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'); this.log.d('Clearing localStorage and sessionStorage');
// Clear only our prefixed keys from localStorage // Clear localStorage with safe iteration
Object.keys(this.localStorage).forEach(key => { this.clearStorageByPrefix(this.localStorage, this.PREFIX_KEY);
if (key.startsWith(this.PREFIX_KEY)) {
this.localStorage.removeItem(key);
}
});
// Clear only our prefixed keys from sessionStorage // Clear sessionStorage with safe iteration
Object.keys(this.sessionStorage).forEach(key => { this.clearStorageByPrefix(this.sessionStorage, this.PREFIX_KEY);
if (key.startsWith(this.PREFIX_KEY)) {
this.sessionStorage.removeItem(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) * Clears only session data (tab-specific data)
*/ */
public clearSessionData(): void { public clearSessionData(): void {
if (!this.isStorageAvailable) return;
this.log.d('Clearing session data'); this.log.d('Clearing session data');
Object.keys(this.sessionStorage).forEach(key => { this.clearStorageByPrefix(this.sessionStorage, this.PREFIX_KEY);
if (key.startsWith(this.PREFIX_KEY)) {
this.sessionStorage.removeItem(key);
}
});
} }
/** /**
* Clears only tab-specific data for current tab * Clears only tab-specific data for current tab
*/ */
public clearTabSpecificData(): void { public clearTabSpecificData(): void {
if (!this.isStorageAvailable) return;
this.log.d('Clearing tab-specific data'); this.log.d('Clearing tab-specific data');
TAB_SPECIFIC_KEYS.forEach(key => { TAB_SPECIFIC_KEYS.forEach((key) => {
this.removeLocalValue(key); this.removeLocalValue(key);
}); });
} }
@ -353,11 +618,17 @@ export class StorageService implements OnDestroy {
* Clears only persistent data * Clears only persistent data
*/ */
public clearPersistentData(): void { public clearPersistentData(): void {
if (!this.isStorageAvailable) return;
this.log.d('Clearing persistent data'); this.log.d('Clearing persistent data');
SHARED_PERSISTENT_KEYS.forEach(key => {
// Clear shared persistent keys
SHARED_PERSISTENT_KEYS.forEach((key) => {
this.removeLocalValue(key); this.removeLocalValue(key);
}); });
TAB_MANAGEMENT_KEYS.forEach(key => {
// Clear tab management keys
TAB_MANAGEMENT_KEYS.forEach((key) => {
this.removeLocalValue(key); this.removeLocalValue(key);
}); });
} }
@ -366,10 +637,24 @@ export class StorageService implements OnDestroy {
* Cleanup method to be called when service is destroyed * Cleanup method to be called when service is destroyed
*/ */
public destroy(): void { public destroy(): void {
// Clear interval
if (this.cleanupInterval) { if (this.cleanupInterval) {
clearInterval(this.cleanupInterval); clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
} }
// Close BroadcastChannel
if (this.broadcastChannel) {
this.broadcastChannel.close();
this.broadcastChannel = null;
}
// Unregister tab
this.unregisterActiveTab(); this.unregisterActiveTab();
// Clear caches
this.cache.clear();
this.cacheTimeout.clear();
} }
/** /**