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'
|
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-';
|
||||||
|
|
|
@ -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');
|
||||||
|
|
||||||
|
// Generate unique tab ID
|
||||||
|
this.tabId = this.generateUniqueTabId();
|
||||||
|
|
||||||
|
// Check storage availability
|
||||||
|
this.isStorageAvailable = this.checkStorageAvailability();
|
||||||
|
|
||||||
|
if (this.isStorageAvailable) {
|
||||||
this.initializeTabManagement();
|
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
|
||||||
|
const keysToRemove = TAB_SPECIFIC_KEYS.map(key => `${this.PREFIX_KEY}${tabId}_${key}`);
|
||||||
|
|
||||||
|
for (const storageKey of keysToRemove) {
|
||||||
|
try {
|
||||||
this.localStorage.removeItem(storageKey);
|
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,32 +335,59 @@ 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
|
||||||
|
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)) {
|
if (SESSION_KEYS.includes(key as StorageKeys)) {
|
||||||
this.setSessionValue(key, item);
|
this.setSessionValue(key, item);
|
||||||
} else {
|
} else {
|
||||||
this.setLocalValue(key, item);
|
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 (!this.isStorageAvailable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
if (SESSION_KEYS.includes(key as StorageKeys)) {
|
if (SESSION_KEYS.includes(key as StorageKeys)) {
|
||||||
return this.getSessionValue(key);
|
return this.getSessionValue(key);
|
||||||
} else {
|
} else {
|
||||||
return this.getLocalValue(key);
|
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)) {
|
if (SESSION_KEYS.includes(key as StorageKeys)) {
|
||||||
this.removeSessionValue(key);
|
this.removeSessionValue(key);
|
||||||
} else {
|
} else {
|
||||||
this.removeLocalValue(key);
|
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}`;
|
||||||
|
|
||||||
|
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);
|
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}`;
|
||||||
|
|
||||||
|
// 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);
|
const str = this.localStorage.getItem(storageKey);
|
||||||
if (!!str) {
|
if (str) {
|
||||||
return JSON.parse(str).item;
|
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}`;
|
||||||
|
|
||||||
|
try {
|
||||||
this.localStorage.removeItem(storageKey);
|
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 {
|
||||||
|
if (!this.isStorageAvailable) return;
|
||||||
|
|
||||||
|
try {
|
||||||
const value = JSON.stringify({ item: item });
|
const value = JSON.stringify({ item: item });
|
||||||
this.sessionStorage.setItem(this.PREFIX_KEY + key, value);
|
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 {
|
||||||
|
if (!this.isStorageAvailable) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
const str = this.sessionStorage.getItem(this.PREFIX_KEY + key);
|
const str = this.sessionStorage.getItem(this.PREFIX_KEY + key);
|
||||||
if (!!str) {
|
if (str) {
|
||||||
return JSON.parse(str).item;
|
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 {
|
||||||
|
if (!this.isStorageAvailable) return;
|
||||||
|
|
||||||
|
try {
|
||||||
this.sessionStorage.removeItem(this.PREFIX_KEY + key);
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue