Troubleshooting

Ecosistema completo para organización y gestión de contenido con interfaces espaciales

UX Widgets Troubleshooting Guide

This guide addresses common issues that may arise when working with the UX Widgets system and provides detailed solutions to resolve them.

Common Issues

Circular Update Loops

Symptoms

Diagnosis

Circular update loops typically occur when:

  1. Component A saves a configuration
  2. Storage service notifies all subscribers about the update
  3. Component B processes the update and saves its own version
  4. Storage service notifies all subscribers again
  5. Component A processes the update and saves its version again
  6. The cycle continues indefinitely

Solutions

1. Implement Source Tracking

Add a source identifier to all save operations:

// When saving
this.storageService.saveConfig(id, config, items, 'component-a');

// When processing updates
this.storageService.configUpdated$.subscribe(update => {
  // Skip self-triggered updates
  if (update.source === 'component-a') {
    return;
  }
  
  // Process the update
});

2. Add Change Detection

Only save when there are actual changes:

// Create a hash of the current state
private getConfigHash(config: any, items: any[]): string {
  const data = JSON.stringify({ config, items });
  return this.hashString(data);
}

// Before saving
const newHash = this.getConfigHash(config, items);
if (newHash === this.lastSavedHash) {
  return; // No changes, don't save
}

// Save and update hash
this.storageService.saveConfig(id, config, items, 'component-a');
this.lastSavedHash = newHash;

3. Implement Update Debouncing

Use RxJS operators to debounce rapid updates:

// Import operators
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';

// In component
this.storageService.configUpdated$
  .pipe(
    debounceTime(300), // Wait 300ms before processing
    distinctUntilChanged((prev, curr) => {
      // Compare important properties only
      return prev.item.config.theme === curr.item.config.theme;
    }),
    takeUntil(this.destroy$)
  )
  .subscribe(update => {
    // Process the update
  });

Theme Persistence Issues

Symptoms

Diagnosis

Theme persistence issues typically occur when:

  1. Theme is stored incorrectly or inconsistently
  2. Theme is applied to wrong elements
  3. Multiple components are applying conflicting themes
  4. CSS specificity issues prevent theme application

Solutions

1. Centralize Theme Storage

Store theme in a single, consistent location:

// In storage service
private readonly THEME_KEY = 'ux-widgets-theme';

saveTheme(theme: string): void {
  localStorage.setItem(this.THEME_KEY, theme);
}

getTheme(): string {
  return localStorage.getItem(this.THEME_KEY) || 'default';
}

2. Separate Component and Global Themes

For components that should manage their own theme vs. global theme:

setThemeClass(theme: string): void {
  const dashboardId = this.dashboardConfig().id;
  
  // Remove all theme classes first
  this.renderer.removeClass(this.elementRef.nativeElement, 'theme-light');
  this.renderer.removeClass(this.elementRef.nativeElement, 'theme-dark');
  this.renderer.removeClass(this.elementRef.nativeElement, 'theme-blue');
  // Add more theme classes as needed
  
  // Add the new theme class
  this.renderer.addClass(this.elementRef.nativeElement, `theme-${theme}`);
  
  // If this is the main dashboard, also apply to body for global effect
  if (dashboardId === 'main-ux-dashboard') {
    document.body.classList.remove('theme-light', 'theme-dark', 'theme-blue');
    document.body.classList.add(`theme-${theme}`);
  }
}

3. Use Host Binding for Component Themes

Apply themes directly to component host elements:

import { Component, HostBinding, Input } from '@angular/core';

@Component({
  selector: 'app-themed-component',
  template: '...'
})
export class ThemedComponent {
  @Input() theme: string = 'default';
  
  @HostBinding('class')
  get hostClasses(): string {
    return `theme-${this.theme}`;
  }
}

4. Implement Theme Service with Distinct Scopes

Create a service that manages themes at different scopes:

@Injectable({ providedIn: 'root' })
export class ThemeService {
  private globalTheme = signal('default');
  private componentThemes = new Map<string, string>();
  
  setGlobalTheme(theme: string): void {
    this.globalTheme.set(theme);
    document.body.className = `theme-${theme}`;
    localStorage.setItem('global-theme', theme);
  }
  
  getGlobalTheme(): string {
    return this.globalTheme();
  }
  
  setComponentTheme(componentId: string, theme: string): void {
    this.componentThemes.set(componentId, theme);
    localStorage.setItem(`component-theme-${componentId}`, theme);
  }
  
  getComponentTheme(componentId: string): string {
    return this.componentThemes.get(componentId) || this.globalTheme();
  }
}

Multiple Instance Conflicts

Symptoms

Diagnosis

Multiple instance conflicts occur when:

  1. Components with the same ID exist simultaneously
  2. Components incorrectly process updates meant for other instances
  3. Global resources (like CSS classes on body) are modified by multiple components

Solutions

1. Ensure Unique Component IDs

Generate or enforce unique IDs for each component instance:

// In component
ngOnInit(): void {
  // If ID not provided, generate unique one
  if (!this.id) {
    this.id = `ux-dashboard-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
  }
  
  // Initialize with unique ID
  this.initialize(this.id);
}

2. Use Component-Specific CSS Classes

Apply styles to component host instead of global elements:

// Before (problematic)
setTheme(theme: string): void {
  document.body.className = `theme-${theme}`;
}

// After (improved)
setTheme(theme: string): void {
  this.renderer.setAttribute(this.elementRef.nativeElement, 'data-theme', theme);
  this.renderer.addClass(this.elementRef.nativeElement, `theme-${theme}`);
}

3. Filter Storage Updates by Component ID

Only process updates relevant to the current component:

this.storageService.configUpdated$
  .pipe(
    // Only process updates for this component
    filter(update => update.item.id === this.componentId),
    takeUntil(this.destroy$)
  )
  .subscribe(update => {
    // Process the update
  });

Storage Initialization Failures

Symptoms

Diagnosis

Storage initialization failures typically occur when:

  1. IndexedDB isn't available in the current environment
  2. Database version conflicts occur
  3. Database operations are attempted before initialization is complete
  4. Browser permissions or privacy settings restrict storage access

Solutions

1. Implement Graceful Fallbacks

Fall back to localStorage when IndexedDB isn't available:

initializeStorage(): void {
  this.initializeIndexedDB().pipe(
    catchError(error => {
      console.warn('IndexedDB initialization failed, using localStorage fallback', error);
      this.usingFallback = true;
      return of(false);
    })
  ).subscribe(initialized => {
    this.storageReady.next(true);
  });
}

saveConfig(id: string, config: any): Observable<boolean> {
  if (this.usingFallback) {
    this.saveToLocalStorage(id, config);
    return of(true);
  }
  
  return this.saveToIndexedDB(id, config);
}

2. Ensure Operations Wait for Initialization

Use a ready signal to delay operations until storage is initialized:

// In storage service
private storageReady = new BehaviorSubject<boolean>(false);
public storageReady$ = this.storageReady.asObservable();

// In component
ngOnInit(): void {
  this.storageService.storageReady$
    .pipe(
      filter(ready => ready), // Wait until ready
      take(1), // Only take the first ready signal
      takeUntil(this.destroy$)
    )
    .subscribe(() => {
      this.loadConfiguration();
    });
}

3. Handle Permissions Issues

Detect and handle permission issues:

private checkStoragePermission(): Observable<boolean> {
  return new Observable<boolean>(observer => {
    try {
      const request = indexedDB.open('permission-test');
      
      request.onsuccess = () => {
        request.result.close();
        indexedDB.deleteDatabase('permission-test');
        observer.next(true);
        observer.complete();
      };
      
      request.onerror = () => {
        observer.next(false);
        observer.complete();
      };
    } catch (error) {
      observer.next(false);
      observer.complete();
    }
  });
}

Performance Degradation

Symptoms

Diagnosis

Performance degradation typically occurs when:

  1. Too many storage operations are performed in quick succession
  2. Large configurations are frequently serialized/deserialized
  3. Inefficient change detection triggers too many updates
  4. Memory leaks from unmanaged subscriptions

Solutions

1. Batch Storage Operations

Group multiple changes into a single save operation:

// Instead of saving after each small change
onItemMoved(): void {
  this.pendingChanges.add('itemsMoved');
  this.scheduleConfigSave();
}

onTitleChanged(): void {
  this.pendingChanges.add('titleChanged');
  this.scheduleConfigSave();
}

private scheduleConfigSave(): void {
  if (this.saveTimeout) {
    clearTimeout(this.saveTimeout);
  }
  
  this.saveTimeout = setTimeout(() => {
    this.saveConfiguration();
    this.pendingChanges.clear();
    this.saveTimeout = null;
  }, 500);
}

2. Implement Lazy Loading

Load configurations only when needed:

@Component({
  selector: 'app-lazy-dashboard',
  template: `
    <div *ngIf="isVisible">
      <app-dashboard [id]="dashboardId"></app-dashboard>
    </div>
    <button (click)="isVisible = !isVisible">Toggle Dashboard</button>
  `
})
export class LazyDashboardComponent {
  isVisible = false;
  dashboardId = 'my-dashboard';
}

3. Use OnPush Change Detection

Reduce unnecessary change detection:

@Component({
  selector: 'app-efficient-widget',
  template: '...',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class EfficientWidgetComponent {
  // Configuration as signals
  config = signal<WidgetConfig>({});
  
  constructor(private cdr: ChangeDetectorRef) {}
  
  updateConfig(newConfig: WidgetConfig): void {
    this.config.set(newConfig);
    this.cdr.markForCheck(); // Manually mark for check
  }
}

Component Rendering Issues

Symptoms

Diagnosis

Rendering issues typically occur when:

  1. Components render before configurations are loaded
  2. CSS conflicts between multiple instances
  3. Third-party library conflicts
  4. Zone.js issues with async operations

Solutions

1. Use Loading States

Show loading indicators until data is ready:

@Component({
  selector: 'app-loading-widget',
  template: `
    <div *ngIf="isLoading" class="loading-indicator">Loading...</div>
    <div *ngIf="!isLoading" class="widget-content">
      <!-- Widget content -->
    </div>
  `
})
export class LoadingWidgetComponent {
  isLoading = true;
  
  ngOnInit(): void {
    this.loadConfiguration().pipe(
      finalize(() => this.isLoading = false)
    ).subscribe();
  }
}

2. Apply CSS Encapsulation

Use Angular's view encapsulation or CSS modules:

@Component({
  selector: 'app-encapsulated-widget',
  template: '...',
  styles: [`
    :host {
      display: block;
      position: relative;
    }
    
    /* Component-specific styles */
  `],
  encapsulation: ViewEncapsulation.Emulated
})
export class EncapsulatedWidgetComponent {
  // Component logic
}

3. Ensure Zone.js Integration

Run operations inside the Angular zone when needed:

private updateOutsideAngular(): void {
  this.ngZone.runOutsideAngular(() => {
    // Expensive operation that doesn't need to trigger change detection
    this.performExpensiveCalculation();
    
    // When done, run inside Angular zone if UI update needed
    this.ngZone.run(() => {
      this.updateComplete = true;
    });
  });
}

IndexedDB Troubleshooting

Common IndexedDB Errors and Solutions

1. QuotaExceededError

Error: QuotaExceededError: The current transaction has exceeded its quota limitations.

Solution:

navigator.storage.persist().then(persistent => {
  if (persistent) {
    console.log('Storage will not be cleared except by explicit user action');
  } else {
    console.log('Storage may be cleared by the UA under storage pressure');
  }
});

2. VersionError

Error: VersionError: The requested database version is less than the existing database version.

Solution:

private readonly DATABASE_VERSION = 1; // Must be consistent

// When upgrading, increment this number and handle migrations:
private readonly DATABASE_VERSION = 2;

request.onupgradeneeded = (event) => {
  const db = (event.target as IDBOpenDBRequest).result;
  const oldVersion = event.oldVersion;
  
  if (oldVersion < 1) {
    // Create initial schema
  }
  
  if (oldVersion < 2) {
    // Migrate data for version 2
  }
};

3. InvalidStateError

Error: InvalidStateError: The object is already closed.

Solution:

private getTransaction(db: IDBDatabase, mode: IDBTransactionMode): IDBTransaction {
  // Check if database is still open
  if (db.version === 0) {
    throw new Error('Database connection closed');
  }
  
  return db.transaction(this.STORE_NAME, mode);
}

Debugging Tools and Techniques

1. Storage Inspection

View and modify stored configurations:

  1. Open DevTools (F12)
  2. Go to Application tab
  3. Under Storage, check:
    • IndexedDB
    • Local Storage
    • Session Storage

2. Add Debug Logging

Enable verbose logging for troubleshooting:

@Injectable({ providedIn: 'root' })
export class LoggingService {
  private debugEnabled = false;
  
  enableDebug(): void {
    this.debugEnabled = true;
    localStorage.setItem('debug-enabled', 'true');
  }
  
  debug(source: string, message: string, data?: any): void {
    if (!this.debugEnabled) return;
    
    console.group(`[${source}] ${message}`);
    if (data) console.log(data);
    console.groupEnd();
  }
}

3. Configuration Auditing

Track all configuration changes:

saveConfig(id: string, config: any, source: string): Observable<boolean> {
  // Log all save operations
  console.log(`Saving config for ${id} from ${source}`, config);
  
  return this.actualSaveOperation(id, config, source).pipe(
    tap(success => {
      console.log(`Save result for ${id}: ${success ? 'SUCCESS' : 'FAILED'}`);
    })
  );
}

Emergency Recovery

1. Clear Storage Data

If the application gets into a corrupted state:

clearAllStorage(): void {
  // Clear IndexedDB
  indexedDB.deleteDatabase(this.DATABASE_NAME);
  
  // Clear localStorage keys related to our app
  Object.keys(localStorage).forEach(key => {
    if (key.startsWith(this.STORAGE_PREFIX)) {
      localStorage.removeItem(key);
    }
  });
  
  // Reload application
  window.location.reload();
}

2. Restore Default Configurations

Provide a way to reset to defaults:

restoreDefaultConfigurations(): void {
  const defaultConfigs = this.getDefaultConfigurations();
  
  // Save all default configurations
  from(defaultConfigs).pipe(
    concatMap(config => this.storageService.saveConfig(config.id, config, 'defaults-reset'))
  ).subscribe({
    complete: () => {
      console.log('Default configurations restored');
      window.location.reload();
    }
  });
}

3. Provide Recovery Mode

Add a special URL parameter to enable recovery mode:

ngOnInit(): void {
  // Check for recovery mode
  const urlParams = new URLSearchParams(window.location.search);
  const recoveryMode = urlParams.get('recovery');
  
  if (recoveryMode === 'true') {
    this.enterRecoveryMode();
  } else {
    this.normalInitialization();
  }
}

private enterRecoveryMode(): void {
  console.log('RECOVERY MODE ENABLED');
  // Show recovery UI
  this.recoveryModeActive = true;
  // Disable auto-loading of potentially corrupted configurations
  this.autoLoad = false;
}

Conclusion

When troubleshooting UX Widgets, follow these steps:

  1. Identify the symptom - Understand exactly what's not working
  2. Check the console - Look for error messages or warnings
  3. Inspect storage - Verify configurations are saving and loading correctly
  4. Add debug logging - If needed, enable verbose logging to track interactions
  5. Isolate the issue - Test components in isolation to pinpoint problems
  6. Apply targeted fixes - Use the appropriate solution from this guide
  7. Verify the fix - Test thoroughly to ensure the issue is resolved

By following the techniques in this guide, you can diagnose and resolve most issues that arise when working with the UX Widgets system.