Skip to content
🔵Entwurf (gut)62%
Vollständigkeit:
80%
Korrektheit:
75%
⏳ Noch nicht geprüft

Storage & State Management

Status: ✅ Fully documented

Overview

The storage and state management in p2d2 provides robust solutions for data persistence, state management, and session management. These utilities ensure consistent data retention across browser sessions and enable complex state interactions between different application components.

Main Modules

1. Map State Manager (map-state.ts)

Central state manager for map-related data with event system and persistence.

State Structure

typescript
export interface MapState {
  activeCRS: string;           // Current projection (e.g., "EPSG:3857")
  localCRS: string | undefined; // Local projection for specific municipalities
  selectedCategory: string | null; // Selected category
  selectedKommune: any | null;     // Current municipality
  isInitialized: boolean;          // Initialization status
}

export interface MapConfig {
  defaultCRS: string;    // Default projection
  wmsUrl: string;        // WMS Service URL
  wmsLayer: string;      // WMS Layer Name
}

State Management Class

typescript
class MapStateManager {
  private state: MapState;
  private config: MapConfig;
  private listeners: Set<(state: MapState) => void> = new Set();

  // State operations
  getState(): Readonly<MapState>
  updateState(updates: Partial<MapState>): void
  subscribe(listener: (state: MapState) => void): () => void

  // Specific setters
  setActiveCRS(crs: string): void
  setLocalCRS(crs: string | undefined): void
  setSelectedCategory(category: string | null): void
  setSelectedKommune(kommune: any | null): void
  setInitialized(initialized: boolean): void

  // Getters
  getSelectedKommune(): any | null
  getSelectedCategory(): string | null

  // Persistence
  restoreFromStorage(): void
}

2. Event System (events.ts)

Robust event handling with retry mechanism, throttling, and queue management.

Event Types and Queue

typescript
// Standard event types
export const EVENT_KOMMUNEN_FOCUS = "kommunen:focus";

interface QueuedEvent {
  eventName: string;
  detail: any;
  timestamp: number;
  retryCount: number;
  maxRetries: number;
}

// Global event queue
const eventQueue: QueuedEvent[] = [];
let isProcessingQueue = false;

Event Dispatching

typescript
/**
 * Dispatch with throttling and retry mechanism
 */
export function dispatchThrottledEvent(
  eventName: string,
  detail: any = {},
  throttleMs: number = 200
): void

/**
 * Robust Kommunen-Focus-Event-Dispatch
 */
export function dispatchKommunenFocus(detail: KommunenFocusDetail): void

/**
 * Event listener with HMR guard
 */
export function addEventListener(
  eventName: string,
  handler: (event: any) => void,
  options?: AddEventListenerOptions
): void

Storage Helpers

typescript
// Local Storage Keys
const STORAGE_KEYS = {
  SELECTED_CRS: "p2d2_selected_crs",
  SELECTED_KOMMUNE: "p2d2_selected_kommune",
};

// Storage operations
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(): void

3. Tab Persistence (tab-persistence.ts)

Session management for browser tabs with cross-tab communication.

Tab Synchronization

typescript
/**
 * Manages persistent data across browser tabs
 */
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 operations
  setState(state: any): void
  getState(): any
  clearState(): void

  // Cross-tab communication
  syncAcrossTabs(): void
  onStateChange(callback: (state: any) => void): void
}

Practical Usage

Complete State Integration

typescript
import { mapState } from '../utils/map-state';
import { dispatchKommunenFocus, addEventListener } from '../utils/events';
import { TabPersistenceManager } from '../utils/tab-persistence';

// 1. Initialize Map State
mapState.restoreFromStorage();

// 2. Register Event Listeners
addEventListener("kommunen:focus", (event) => {
  const detail = event.detail;
  
  // Update state
  mapState.setSelectedKommune(detail);
  mapState.setLocalCRS(detail.projection);
  
  // Persist
  localStorage.setItem("p2d2_selected_kommune", JSON.stringify(detail));
});

// 3. Initialize Tab Persistence
const tabManager = new TabPersistenceManager();
tabManager.onStateChange((state) => {
  // Synchronize state across tabs
  if (state.selectedKommune) {
    mapState.setSelectedKommune(state.selectedKommune);
  }
});

Tracking State Changes

typescript
// Subscribe to state changes
const unsubscribe = mapState.subscribe((state) => {
  console.log("Map state changed:", {
    crs: state.activeCRS,
    kommune: state.selectedKommune?.slug,
    category: state.selectedCategory
  });
});

// Unsubscribe later
unsubscribe();

Robust Event Handling

typescript
// Event with retry mechanism
try {
  dispatchKommunenFocus({
    center: [6.95, 50.94],
    zoom: 12,
    slug: 'koeln',
    projection: 'EPSG:3857'
  });
} catch (error) {
  console.error("Event dispatch failed:", error);
  // Fallback: Direct state change
  mapState.setSelectedKommune({
    slug: 'koeln',
    center: [6.95, 50.94],
    zoom: 12
  });
}

Configuration

Storage Keys Convention

typescript
// Standard Storage Keys for p2d2
export const STORAGE_KEYS = {
  // Map-related data
  SELECTED_CRS: "p2d2_selected_crs",
  SELECTED_KOMMUNE: "p2d2_selected_kommune",
  SELECTED_CATEGORY: "p2d2_selected_category",
  
  // Layer states
  LUFTSBILD_VISIBLE: "luftbildVisible",
  BASEMAP_VISIBLE: "basemapVisible",
  
  // UI states
  SIDEBAR_OPEN: "p2d2_sidebar_open",
  THEME: "p2d2_theme"
};

Event Configuration

typescript
// Event system settings
const EVENT_CONFIG = {
  MAX_RETRIES: 3,           // Maximum retries
  RETRY_DELAY: 250,         // Delay between retries (ms)
  THROTTLE_MS: 200,         // Default throttling time
  QUEUE_PROCESS_INTERVAL: 100 // Queue processing interval
};

Performance Optimizations

1. State Immutability

typescript
// ✅ Correct - Immutable updates
updateState(updates: Partial<MapState>): void {
  const oldState = { ...this.state };
  this.state = { ...this.state, ...updates };
  this.notifyListeners(oldState, this.state);
}

// ❌ Avoid - Direct mutation
this.state.activeCRS = newCRS; // Avoid direct mutation

2. Selective Re-rendering

typescript
// React only to relevant changes
const unsubscribe = mapState.subscribe((newState) => {
  // Check if the municipality actually changed
  if (newState.selectedKommune?.slug !== currentKommuneSlug) {
    updateKommuneDisplay(newState.selectedKommune);
  }
});

3. Memory Management

typescript
// Proper cleanup of event listeners
class KommuneComponent {
  private eventCleanup: () => void;
  
  constructor() {
    this.eventCleanup = addEventListener("kommunen:focus", this.handleFocus);
  }
  
  destroy() {
    this.eventCleanup(); // Remove listener
  }
}

Error Handling

Robust Storage Handling

typescript
// 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: Delete old data
      this.cleanupOldData();
      return false;
    }
    console.error(`Storage error for key: ${key}`, error);
    return false;
  }
}

Graceful Degradation

typescript
// Fallback for storage errors
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 Normalization

typescript
// ✅ Correct - Normalized state structure
interface NormalizedState {
  kommunen: { [slug: string]: KommuneData };
  categories: { [slug: string]: CategoryData };
  ui: {
    selectedKommuneSlug: string | null;
    selectedCategorySlug: string | null;
    activeCRS: string;
  }
}

// ❌ Avoid - Nested structures
interface NestedState {
  selectedKommune: KommuneData; // Contains all data
  selectedCategory: CategoryData; // Duplicated data
}

2. Event Design

typescript
// ✅ Correct - Clear event payloads
interface KommunenFocusEvent {
  type: "KOMMUNEN_FOCUS";
  payload: {
    slug: string;
    center: [number, number];
    zoom: number;
    projection: string;
  };
}

// ❌ Avoid - Vague event data
// "somethingHappened" with unclear structure

3. Storage Size Management

typescript
// Automatic cleanup
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) {
        // Remove corrupt entries
        localStorage.removeItem(key);
      }
    });
}

Dependencies

External Dependencies

  • BroadcastChannel API - Cross-tab communication (if available)
  • localStorage - Browser persistence

Internal Dependencies

  • ../utils/logger - Consistent logging infrastructure
  • ../config/map-config - Standard configurations

Security Aspects

Data Sanitization

typescript
// Validate and sanitize input data
function sanitizeStorageData(data: any): any {
  // Remove potentially dangerous properties
  const { __proto__, constructor, prototype, ...safeData } = data;
  return safeData;
}

// When saving
const safeData = sanitizeStorageData(userData);
localStorage.setItem(key, JSON.stringify(safeData));

Size Limits

typescript
// Check storage size limits
function checkStorageSize(key: string, data: any): boolean {
  const estimatedSize = JSON.stringify(data).length;
  const MAX_SIZE = 5 * 1024 * 1024; // 5MB per entry
  
  if (estimatedSize > MAX_SIZE) {
    console.warn(`Storage entry too large: ${key} (${estimatedSize} bytes)`);
    return false;
  }
  return true;
}

These Storage and State Management utilities ensure robust, performant, and secure data management in p2d2, with consistent persistence across browser sessions and efficient state coordination between all application components.