Storage & State Management
Status: ✅ Vollständig dokumentiert
Übersicht
Das Storage- und State-Management in p2d2 bietet robuste Lösungen für Datenpersistenz, Zustandsverwaltung und Session-Management. Diese Utilities gewährleisten konsistente Datenhaltung über Browser-Sessions hinweg und ermöglichen komplexe State-Interaktionen zwischen verschiedenen Anwendungskomponenten.
Hauptmodule
1. Map State Manager (map-state.ts)
Zentraler Zustandsmanager für Karten-bezogene Daten mit Event-System und Persistenz.
State-Struktur
export interface MapState {
activeCRS: string; // Aktuelle Projektion (z.B. "EPSG:3857")
localCRS: string | undefined; // Lokale Projektion für spezifische Kommunen
selectedCategory: string | null; // Ausgewählte Kategorie
selectedKommune: any | null; // Aktuelle Kommune
isInitialized: boolean; // Initialisierungsstatus
}
export interface MapConfig {
defaultCRS: string; // Standard-Projektion
wmsUrl: string; // WMS Service URL
wmsLayer: string; // WMS Layer Name
}State-Management-Klasse
class MapStateManager {
private state: MapState;
private config: MapConfig;
private listeners: Set<(state: MapState) => void> = new Set();
// State-Operationen
getState(): Readonly<MapState>
updateState(updates: Partial<MapState>): void
subscribe(listener: (state: MapState) => void): () => void
// Spezifische Setter
setActiveCRS(crs: string): void
setLocalCRS(crs: string | undefined): void
setSelectedCategory(category: string | null): void
setSelectedKommune(kommune: any | null): void
setInitialized(initialized: boolean): void
// Getter
getSelectedKommune(): any | null
getSelectedCategory(): string | null
// Persistenz
restoreFromStorage(): void
}2. Event System (events.ts)
Robustes Event-Handling mit Retry-Mechanismus, Throttling und Queue-Management.
Event-Typen und Queue
// Standard-Event-Typen
export const EVENT_KOMMUNEN_FOCUS = "kommunen:focus";
interface QueuedEvent {
eventName: string;
detail: any;
timestamp: number;
retryCount: number;
maxRetries: number;
}
// Globale Event-Queue
const eventQueue: QueuedEvent[] = [];
let isProcessingQueue = false;Event-Dispatching
/**
* Dispatch mit Throttling und Retry-Mechanismus
*/
export function dispatchThrottledEvent(
eventName: string,
detail: any = {},
throttleMs: number = 200
): void
/**
* Robuste Kommunen-Focus-Event-Dispatch
*/
export function dispatchKommunenFocus(detail: KommunenFocusDetail): void
/**
Event-Listener mit HMR-Guard
*/
export function addEventListener(
eventName: string,
handler: (event: any) => void,
options?: AddEventListenerOptions
): voidStorage-Helper
// Local Storage Keys
const STORAGE_KEYS = {
SELECTED_CRS: "p2d2_selected_crs",
SELECTED_KOMMUNE: "p2d2_selected_kommune",
};
// Storage-Operationen
export function getSelectedCRS(): string | null
export function setSelectedCRS(crs: string): void
export function getSelectedKommune(): string | null
export function setSelectedKommune(slug: string): void
export function clearSelections(): void3. Tab Persistence (tab-persistence.ts)
Session-Management für Browser-Tabs mit Cross-Tab-Kommunikation.
Tab-Synchronisation
/**
* Verwaltet persistente Daten über Browser-Tabs hinweg
*/
export class TabPersistenceManager {
private storageKey: string;
private channel: BroadcastChannel | null;
constructor(storageKey: string = "p2d2_tab_state") {
this.storageKey = storageKey;
this.channel = typeof BroadcastChannel !== 'undefined'
? new BroadcastChannel(storageKey)
: null;
}
// State-Operationen
setState(state: any): void
getState(): any
clearState(): void
// Cross-Tab-Kommunikation
syncAcrossTabs(): void
onStateChange(callback: (state: any) => void): void
}Verwendung in der Praxis
Komplette State-Integration
import { mapState } from '../utils/map-state';
import { dispatchKommunenFocus, addEventListener } from '../utils/events';
import { TabPersistenceManager } from '../utils/tab-persistence';
// 1. Map State initialisieren
mapState.restoreFromStorage();
// 2. Event-Listener registrieren
addEventListener("kommunen:focus", (event) => {
const detail = event.detail;
// State aktualisieren
mapState.setSelectedKommune(detail);
mapState.setLocalCRS(detail.projection);
// Persistieren
localStorage.setItem("p2d2_selected_kommune", JSON.stringify(detail));
});
// 3. Tab-Persistence initialisieren
const tabManager = new TabPersistenceManager();
tabManager.onStateChange((state) => {
// State über Tabs synchronisieren
if (state.selectedKommune) {
mapState.setSelectedKommune(state.selectedKommune);
}
});State-Änderungen verfolgen
// State-Änderungen abonnieren
const unsubscribe = mapState.subscribe((state) => {
console.log("Map State geändert:", {
crs: state.activeCRS,
kommune: state.selectedKommune?.slug,
category: state.selectedCategory
});
});
// Später abmelden
unsubscribe();Robuste Event-Behandlung
// Event mit Retry-Mechanismus
try {
dispatchKommunenFocus({
center: [6.95, 50.94],
zoom: 12,
slug: 'koeln',
projection: 'EPSG:3857'
});
} catch (error) {
console.error("Event-Dispatch fehlgeschlagen:", error);
// Fallback: Direkte State-Änderung
mapState.setSelectedKommune({
slug: 'koeln',
center: [6.95, 50.94],
zoom: 12
});
}Konfiguration
Storage-Keys Konvention
// Standard Storage-Keys für p2d2
export const STORAGE_KEYS = {
// Map-bezogene Daten
SELECTED_CRS: "p2d2_selected_crs",
SELECTED_KOMMUNE: "p2d2_selected_kommune",
SELECTED_CATEGORY: "p2d2_selected_category",
// Layer-Zustände
LUFTSBILD_VISIBLE: "luftbildVisible",
BASEMAP_VISIBLE: "basemapVisible",
// UI-Zustände
SIDEBAR_OPEN: "p2d2_sidebar_open",
THEME: "p2d2_theme"
};Event-Konfiguration
// Event-System Einstellungen
const EVENT_CONFIG = {
MAX_RETRIES: 3, // Maximale Wiederholungsversuche
RETRY_DELAY: 250, // Verzögerung zwischen Retries (ms)
THROTTLE_MS: 200, // Standard-Throttling-Zeit
QUEUE_PROCESS_INTERVAL: 100 // Queue-Verarbeitungsintervall
};Performance-Optimierungen
1. State-Immutation
// ✅ Korrekt - Immutable Updates
updateState(updates: Partial<MapState>): void {
const oldState = { ...this.state };
this.state = { ...this.state, ...updates };
this.notifyListeners(oldState, this.state);
}
// ❌ Vermeiden - Direkte Mutation
this.state.activeCRS = newCRS; // Direkte Mutation vermeiden2. Selective Re-rendering
// Nur bei relevanten Änderungen reagieren
const unsubscribe = mapState.subscribe((newState) => {
// Prüfe ob sich die Kommune wirklich geändert hat
if (newState.selectedKommune?.slug !== currentKommuneSlug) {
updateKommuneDisplay(newState.selectedKommune);
}
});3. Memory Management
// Proper Cleanup von Event-Listenern
class KommuneComponent {
private eventCleanup: () => void;
constructor() {
this.eventCleanup = addEventListener("kommunen:focus", this.handleFocus);
}
destroy() {
this.eventCleanup(); // Listener entfernen
}
}Fehlerbehandlung
Robustes Storage-Handling
// Safe Storage Operations
function safeSetItem(key: string, value: any): boolean {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
if (error instanceof DOMException && error.name === 'QuotaExceededError') {
console.warn(`Storage quota exceeded for key: ${key}`);
// Fallback: Alte Daten löschen
this.cleanupOldData();
return false;
}
console.error(`Storage error for key: ${key}`, error);
return false;
}
}Graceful Degradation
// Fallback bei Storage-Fehlern
function getWithFallback<T>(key: string, fallback: T): T {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : fallback;
} catch (error) {
console.warn(`Failed to read ${key}, using fallback`, error);
return fallback;
}
}Best Practices
1. State-Normalisierung
// ✅ Korrekt - Normalisierte State-Struktur
interface NormalizedState {
kommunen: { [slug: string]: KommuneData };
categories: { [slug: string]: CategoryData };
ui: {
selectedKommuneSlug: string | null;
selectedCategorySlug: string | null;
activeCRS: string;
}
}
// ❌ Vermeiden - Verschachtelte Strukturen
interface NestedState {
selectedKommune: KommuneData; // Enthält alle Daten
selectedCategory: CategoryData; // Duplizierte Daten
}2. Event-Design
// ✅ Korrekt - Klare Event-Payloads
interface KommunenFocusEvent {
type: "KOMMUNEN_FOCUS";
payload: {
slug: string;
center: [number, number];
zoom: number;
projection: string;
};
}
// ❌ Vermeiden - Vage Event-Daten
// "somethingHappened" mit unklarer Struktur3. Storage-Size Management
// Automatische Bereinigung
function cleanupOldData(): void {
const oneWeekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
Object.keys(localStorage)
.filter(key => key.startsWith("p2d2_"))
.forEach(key => {
try {
const item = localStorage.getItem(key);
if (item) {
const data = JSON.parse(item);
if (data.timestamp && data.timestamp < oneWeekAgo) {
localStorage.removeItem(key);
}
}
} catch (error) {
// Fehlerhafte Einträge entfernen
localStorage.removeItem(key);
}
});
}Abhängigkeiten
Externe Abhängigkeiten
- BroadcastChannel API - Cross-Tab-Kommunikation (falls verfügbar)
- localStorage - Browser-Persistenz
Interne Abhängigkeiten
../utils/logger- Konsistente Logging-Infrastruktur../config/map-config- Standard-Konfigurationen
Sicherheitsaspekte
Data Sanitization
// Eingabedaten validieren und bereinigen
function sanitizeStorageData(data: any): any {
// Entferne potenziell gefährliche Eigenschaften
const { __proto__, constructor, prototype, ...safeData } = data;
return safeData;
}
// Beim Speichern
const safeData = sanitizeStorageData(userData);
localStorage.setItem(key, JSON.stringify(safeData));Size Limits
// Prüfe Storage-Größenbeschränkungen
function checkStorageSize(key: string, data: any): boolean {
const estimatedSize = JSON.stringify(data).length;
const MAX_SIZE = 5 * 1024 * 1024; // 5MB pro Eintrag
if (estimatedSize > MAX_SIZE) {
console.warn(`Storage entry too large: ${key} (${estimatedSize} bytes)`);
return false;
}
return true;
}Diese Storage- und State-Management-Utilities gewährleisten eine robuste, performante und sichere Datenverwaltung in p2d2, mit konsistenter Persistenz über Browser-Sessions hinweg und effizienter State-Koordination zwischen allen Anwendungskomponenten.