mirror of https://github.com/OpenVidu/openvidu.git
ov-components: optimize storage service for improved performance and error handling
parent
f6d1b6e86c
commit
0ba70638e6
|
@ -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-';
|
||||
|
|
|
@ -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<string, any>();
|
||||
private cacheTimeout = new Map<string, number>();
|
||||
private readonly CACHE_TTL = 5000; // 5 seconds cache TTL
|
||||
|
||||
constructor(protected loggerSrv: LoggerService) {
|
||||
this.log = this.loggerSrv.get('StorageService');
|
||||
|
||||
// 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}`;
|
||||
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,32 +335,59 @@ export class StorageService implements OnDestroy {
|
|||
return this.get(StorageKeys.BACKGROUND);
|
||||
}
|
||||
|
||||
removeBackground() {
|
||||
removeBackground(): void {
|
||||
this.remove(StorageKeys.BACKGROUND);
|
||||
}
|
||||
|
||||
protected set(key: string, item: any) {
|
||||
// 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 (!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) {
|
||||
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}`;
|
||||
|
||||
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}`;
|
||||
|
||||
// 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) {
|
||||
return JSON.parse(str).item;
|
||||
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}`;
|
||||
|
||||
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 {
|
||||
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 {
|
||||
if (!this.isStorageAvailable) return null;
|
||||
|
||||
try {
|
||||
const str = this.sessionStorage.getItem(this.PREFIX_KEY + key);
|
||||
if (!!str) {
|
||||
return JSON.parse(str).item;
|
||||
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 {
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue