mirror of https://github.com/OpenVidu/openvidu.git
ov-components: refactor(storage): Enhance tab management and cleanup mechanisms in StorageService
parent
b1d0269211
commit
e31a78d153
|
@ -19,7 +19,6 @@ import { TranslateService } from '../../services/translate/translate.service';
|
||||||
import { LocalTrack } from 'livekit-client';
|
import { LocalTrack } from 'livekit-client';
|
||||||
import { CustomDevice } from '../../models/device.model';
|
import { CustomDevice } from '../../models/device.model';
|
||||||
import { LangOption } from '../../models/lang.model';
|
import { LangOption } from '../../models/lang.model';
|
||||||
import { StorageService } from '../../services/storage/storage.service';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
@ -74,7 +73,6 @@ export class PreJoinComponent implements OnInit, OnDestroy {
|
||||||
private libService: OpenViduComponentsConfigService,
|
private libService: OpenViduComponentsConfigService,
|
||||||
private cdkSrv: CdkOverlayService,
|
private cdkSrv: CdkOverlayService,
|
||||||
private openviduService: OpenViduService,
|
private openviduService: OpenViduService,
|
||||||
private storageService: StorageService,
|
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private changeDetector: ChangeDetectorRef
|
private changeDetector: ChangeDetectorRef
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -9,7 +9,36 @@ export enum StorageKeys {
|
||||||
CAMERA_ENABLED = 'cameraEnabled',
|
CAMERA_ENABLED = 'cameraEnabled',
|
||||||
LANG = 'lang',
|
LANG = 'lang',
|
||||||
CAPTION_LANG = 'captionLang',
|
CAPTION_LANG = 'captionLang',
|
||||||
BACKGROUND = "virtualBg"
|
BACKGROUND = 'virtualBg',
|
||||||
|
TAB_ID = 'tabId',
|
||||||
|
ACTIVE_TABS = 'activeTabs'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PERSISTENT_KEYS: StorageKeys[] = [
|
||||||
|
StorageKeys.VIDEO_DEVICE,
|
||||||
|
StorageKeys.AUDIO_DEVICE,
|
||||||
|
StorageKeys.LANG,
|
||||||
|
StorageKeys.CAPTION_LANG,
|
||||||
|
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,6 @@
|
||||||
import { Injectable } 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 } 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';
|
||||||
|
|
||||||
|
@ -10,13 +10,125 @@ import { CustomDevice } from '../../models/device.model';
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class StorageService {
|
export class StorageService implements OnDestroy {
|
||||||
public storage = window.localStorage;
|
public localStorage = window.localStorage;
|
||||||
|
public sessionStorage = window.sessionStorage;
|
||||||
public log: ILogger;
|
public log: ILogger;
|
||||||
protected PREFIX_KEY = STORAGE_PREFIX;
|
protected PREFIX_KEY = STORAGE_PREFIX;
|
||||||
|
private tabId: string;
|
||||||
|
private readonly TAB_CLEANUP_INTERVAL = 30000; // 30 seconds
|
||||||
|
private cleanupInterval: any;
|
||||||
|
|
||||||
constructor(protected loggerSrv: LoggerService) {
|
constructor(protected loggerSrv: LoggerService) {
|
||||||
this.log = this.loggerSrv.get('StorageService');
|
this.log = this.loggerSrv.get('StorageService');
|
||||||
|
this.initializeTabManagement();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes tab management system
|
||||||
|
* Creates unique tab ID and sets up cleanup mechanism
|
||||||
|
*/
|
||||||
|
private initializeTabManagement(): void {
|
||||||
|
// Generate unique tab ID
|
||||||
|
this.tabId = this.generateTabId();
|
||||||
|
this.setSessionValue(StorageKeys.TAB_ID, this.tabId);
|
||||||
|
|
||||||
|
// Register this tab as active
|
||||||
|
this.registerActiveTab();
|
||||||
|
|
||||||
|
// Set up periodic cleanup of inactive tabs
|
||||||
|
this.setupTabCleanup();
|
||||||
|
|
||||||
|
// Listen for page unload to clean up this tab
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
this.unregisterActiveTab();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.log.d(`Tab initialized with ID: ${this.tabId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique tab identifier
|
||||||
|
*/
|
||||||
|
private generateTabId(): string {
|
||||||
|
return `tab_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers current tab as active
|
||||||
|
*/
|
||||||
|
private registerActiveTab(): void {
|
||||||
|
const activeTabs = this.getActiveTabsFromStorage() || {};
|
||||||
|
activeTabs[this.tabId] = Date.now();
|
||||||
|
this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters current tab from active tabs
|
||||||
|
*/
|
||||||
|
private unregisterActiveTab(): void {
|
||||||
|
const activeTabs = this.getActiveTabsFromStorage() || {};
|
||||||
|
delete activeTabs[this.tabId];
|
||||||
|
this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs);
|
||||||
|
this.cleanupTabData(this.tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up periodic cleanup of inactive tabs
|
||||||
|
*/
|
||||||
|
private setupTabCleanup(): void {
|
||||||
|
this.cleanupInterval = setInterval(() => {
|
||||||
|
this.cleanupInactiveTabs();
|
||||||
|
}, this.TAB_CLEANUP_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up data from inactive tabs
|
||||||
|
*/
|
||||||
|
private cleanupInactiveTabs(): void {
|
||||||
|
const activeTabs = this.getActiveTabsFromStorage() || {};
|
||||||
|
const currentTime = Date.now();
|
||||||
|
const timeoutThreshold = this.TAB_CLEANUP_INTERVAL * 2; // 60 seconds
|
||||||
|
|
||||||
|
Object.keys(activeTabs).forEach(tabId => {
|
||||||
|
const lastActivity = activeTabs[tabId];
|
||||||
|
if (currentTime - lastActivity > timeoutThreshold) {
|
||||||
|
this.log.d(`Cleaning up inactive tab: ${tabId}`);
|
||||||
|
delete activeTabs[tabId];
|
||||||
|
this.cleanupTabData(tabId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update heartbeat for current tab
|
||||||
|
activeTabs[this.tabId] = currentTime;
|
||||||
|
this.setLocalValue(StorageKeys.ACTIVE_TABS, activeTabs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up data associated with a specific tab
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.log.d(`Cleaned up data for tab: ${tabId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets active tabs from localStorage
|
||||||
|
*/
|
||||||
|
private getActiveTabsFromStorage(): { [key: string]: number } | null {
|
||||||
|
return this.getLocalValue(StorageKeys.ACTIVE_TABS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current tab ID
|
||||||
|
*/
|
||||||
|
public getTabId(): string {
|
||||||
|
return this.tabId;
|
||||||
}
|
}
|
||||||
|
|
||||||
getParticipantName(): string | null {
|
getParticipantName(): string | null {
|
||||||
|
@ -106,24 +218,164 @@ export class StorageService {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected set(key: string, item: any) {
|
protected set(key: string, item: any) {
|
||||||
const value = JSON.stringify({ item: item });
|
if (SESSION_KEYS.includes(key as StorageKeys)) {
|
||||||
this.storage.setItem(this.PREFIX_KEY + key, value);
|
this.setSessionValue(key, item);
|
||||||
|
} else {
|
||||||
|
this.setLocalValue(key, item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get(key: string): any {
|
protected get(key: string): any {
|
||||||
const str = this.storage.getItem(this.PREFIX_KEY + key);
|
if (SESSION_KEYS.includes(key as StorageKeys)) {
|
||||||
|
return this.getSessionValue(key);
|
||||||
|
} else {
|
||||||
|
return this.getLocalValue(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected remove(key: string) {
|
||||||
|
if (SESSION_KEYS.includes(key as StorageKeys)) {
|
||||||
|
this.removeSessionValue(key);
|
||||||
|
} else {
|
||||||
|
this.removeLocalValue(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a key should use tab-specific storage in localStorage
|
||||||
|
*/
|
||||||
|
private shouldUseTabSpecificKey(key: string): boolean {
|
||||||
|
return TAB_SPECIFIC_KEYS.includes(key as StorageKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets value in localStorage with tab-specific key if needed
|
||||||
|
*/
|
||||||
|
private setLocalValue(key: string, item: any): void {
|
||||||
|
const value = JSON.stringify({ item: item });
|
||||||
|
const storageKey = this.shouldUseTabSpecificKey(key)
|
||||||
|
? `${this.PREFIX_KEY}${this.tabId}_${key}`
|
||||||
|
: `${this.PREFIX_KEY}${key}`;
|
||||||
|
this.localStorage.setItem(storageKey, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets value from localStorage with tab-specific key if needed
|
||||||
|
*/
|
||||||
|
private getLocalValue(key: string): any {
|
||||||
|
const storageKey = this.shouldUseTabSpecificKey(key)
|
||||||
|
? `${this.PREFIX_KEY}${this.tabId}_${key}`
|
||||||
|
: `${this.PREFIX_KEY}${key}`;
|
||||||
|
const str = this.localStorage.getItem(storageKey);
|
||||||
if (!!str) {
|
if (!!str) {
|
||||||
return JSON.parse(str).item;
|
return JSON.parse(str).item;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected remove(key: string) {
|
/**
|
||||||
this.storage.removeItem(this.PREFIX_KEY + key);
|
* Removes value from localStorage with tab-specific key if needed
|
||||||
|
*/
|
||||||
|
private removeLocalValue(key: string): void {
|
||||||
|
const storageKey = this.shouldUseTabSpecificKey(key)
|
||||||
|
? `${this.PREFIX_KEY}${this.tabId}_${key}`
|
||||||
|
: `${this.PREFIX_KEY}${key}`;
|
||||||
|
this.localStorage.removeItem(storageKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets value in sessionStorage
|
||||||
|
*/
|
||||||
|
private setSessionValue(key: string, item: any): void {
|
||||||
|
const value = JSON.stringify({ item: item });
|
||||||
|
this.sessionStorage.setItem(this.PREFIX_KEY + key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets value from sessionStorage
|
||||||
|
*/
|
||||||
|
private getSessionValue(key: string): any {
|
||||||
|
const str = this.sessionStorage.getItem(this.PREFIX_KEY + key);
|
||||||
|
if (!!str) {
|
||||||
|
return JSON.parse(str).item;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes value from sessionStorage
|
||||||
|
*/
|
||||||
|
private removeSessionValue(key: string): void {
|
||||||
|
this.sessionStorage.removeItem(this.PREFIX_KEY + key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public clear() {
|
public clear() {
|
||||||
this.log.d('Clearing localStorage');
|
this.log.d('Clearing localStorage and sessionStorage');
|
||||||
this.storage.clear();
|
|
||||||
|
// Clear only our prefixed keys from localStorage
|
||||||
|
Object.keys(this.localStorage).forEach(key => {
|
||||||
|
if (key.startsWith(this.PREFIX_KEY)) {
|
||||||
|
this.localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear only our prefixed keys from sessionStorage
|
||||||
|
Object.keys(this.sessionStorage).forEach(key => {
|
||||||
|
if (key.startsWith(this.PREFIX_KEY)) {
|
||||||
|
this.sessionStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears only session data (tab-specific data)
|
||||||
|
*/
|
||||||
|
public clearSessionData(): void {
|
||||||
|
this.log.d('Clearing session data');
|
||||||
|
Object.keys(this.sessionStorage).forEach(key => {
|
||||||
|
if (key.startsWith(this.PREFIX_KEY)) {
|
||||||
|
this.sessionStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears only tab-specific data for current tab
|
||||||
|
*/
|
||||||
|
public clearTabSpecificData(): void {
|
||||||
|
this.log.d('Clearing tab-specific data');
|
||||||
|
TAB_SPECIFIC_KEYS.forEach(key => {
|
||||||
|
this.removeLocalValue(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears only persistent data
|
||||||
|
*/
|
||||||
|
public clearPersistentData(): void {
|
||||||
|
this.log.d('Clearing persistent data');
|
||||||
|
SHARED_PERSISTENT_KEYS.forEach(key => {
|
||||||
|
this.removeLocalValue(key);
|
||||||
|
});
|
||||||
|
TAB_MANAGEMENT_KEYS.forEach(key => {
|
||||||
|
this.removeLocalValue(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup method to be called when service is destroyed
|
||||||
|
*/
|
||||||
|
public destroy(): void {
|
||||||
|
if (this.cleanupInterval) {
|
||||||
|
clearInterval(this.cleanupInterval);
|
||||||
|
}
|
||||||
|
this.unregisterActiveTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Angular lifecycle hook - called when service is destroyed
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue