Events & Signals

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

📡 Reactive Events & Signals

Complete guide to the reactive events system and Angular signals integration for real-time storage synchronization.


🔄 Cross-Tab Synchronization

Automatic Sync Mechanism

The system uses BroadcastChannel to synchronize data across all browser tabs in real-time:

// Tab 1: Save data
await storage.put('user/theme', 'dark');

// Tab 2: Automatically updates within ~200ms
const theme = userTheme(); // 'dark' - no manual refresh needed!

How It Works

  1. BroadcastChannel: Uses BroadcastChannel('gg-storage:<namespace>') for communication
  2. Namespace Isolation: Each namespace gets its own channel for perfect isolation
  3. Event Propagation: Changes broadcast to all tabs automatically
  4. Signal Updates: Storage signals update automatically across all tabs

Namespace Isolation

Each namespace gets its own broadcast channel:

// App A (namespace: 'app-a')
storage.put('user/data', dataA); // Broadcasts on 'gg-storage:app-a'

// App B (namespace: 'app-b') 
storage.put('user/data', dataB); // Broadcasts on 'gg-storage:app-b'
// No conflicts - completely isolated

⚡ Angular Signals Integration

Reactive Storage Signals

Create reactive signals that automatically update when storage changes:

@Component({
  selector: 'app-user-profile',
  template: `
    <div class="profile">
      @if (userProfile(); as profile) {
        <h1>{{ profile.name }}</h1>
        <p>{{ profile.email }}</p>
      } @else {
        <p>Loading profile...</p>
      }
      <button (click)="updateProfile()">Update</button>
    </div>
  `
})
export class UserProfileComponent {
  private storage = inject(StorageService);

  // Reactive signal - automatically updates UI when storage changes
  readonly userProfile = this.storage.asSignal<UserProfile>('user/profile', null).value;

  async updateProfile() {
    await this.storage.put('user/profile', {
      name: 'Jane Doe',
      email: '[email protected]',
      updatedAt: new Date().toISOString()
    });
    // UI automatically updates across ALL tabs!
  }
}

Signal Features

// Create reactive signal with default value
const userSettings = storage.asSignal<UserSettings>('user/settings', {
  theme: 'light',
  language: 'en'
});

// Access current value (reactive)
console.log(userSettings.value()); // Current settings

// Manually refresh from storage (rarely needed)
await userSettings.refresh();

// Signal automatically updates when:
// 1. Storage changes in current tab
// 2. Storage changes in other tabs  
// 3. Storage changes from sync operations

📡 Event System

Storage Events

The system emits events for all storage operations:

type StorageEvent = {
  type: 'put' | 'remove' | 'clear';
  key?: string; // undefined for clear operations
  value?: any;  // included for put operations
  timestamp: number;
};

// Listen for all storage events
const unsubscribe = storage.onEvent((event) => {
  console.log('Storage event:', event);
  
  switch (event.type) {
    case 'put':
      console.log(`Data saved: ${event.key}`);
      break;
    case 'remove':
      console.log(`Data removed: ${event.key}`);
      break;
    case 'clear':
      console.log(`Storage cleared: ${event.key || 'all'}`);
      break;
  }
});

// Cleanup when done
unsubscribe();

Event Filtering

Listen for specific keys or patterns:

storage.onEvent((event) => {
  // Only handle user-related events
  if (event.key?.startsWith('user/')) {
    console.log('User data changed:', event);
    
    // Trigger side effects
    if (event.key === 'user/theme') {
      applyTheme(event.value);
    }
  }
});

🎯 Advanced Patterns

Multi-Component Synchronization

Multiple components automatically stay in sync:

// Header component
@Component({
  selector: 'app-header',
  template: `<h1>{{ appTitle() }}</h1>`
})
export class HeaderComponent {
  appTitle = inject(StorageService).asSignal<string>('app/title', 'My App').value;
}

// Settings component  
@Component({
  selector: 'app-settings',
  template: `
    <input [value]="appTitle()" 
           (input)="updateTitle($any($event.target).value)">
  `
})
export class SettingsComponent {
  private storage = inject(StorageService);
  appTitle = this.storage.asSignal<string>('app/title', 'My App').value;

  async updateTitle(value: string) {
    await this.storage.put('app/title', value);
    // Header automatically updates!
  }
}

Computed Reactive State

Create derived state from storage signals:

@Component({
  selector: 'app-dashboard',
  template: `
    <div [attr.data-theme]="currentTheme()">
      <h1>{{ welcomeMessage() }}</h1>
      <p>{{ statusMessage() }}</p>
    </div>
  `
})
export class DashboardComponent {
  private storage = inject(StorageService);

  // Base storage signals
  userProfile = this.storage.asSignal<UserProfile>('user/profile', null).value;
  userSettings = this.storage.asSignal<UserSettings>('user/settings', { 
    theme: 'light' 
  }).value;

  // Computed signals derived from storage
  currentTheme = computed(() => this.userSettings().theme);
  
  welcomeMessage = computed(() => {
    const profile = this.userProfile();
    return profile ? `Welcome back, ${profile.name}!` : 'Welcome!';
  });
  
  statusMessage = computed(() => {
    const profile = this.userProfile();
    const settings = this.userSettings();
    return `Theme: ${settings.theme}, User: ${profile?.name || 'Anonymous'}`;
  });
}

🔄 Bulk Operations & Events

Prefix-Based Updates

Use prefixes to efficiently update groups of related signals:

// Clear all user data
await storage.clear('user/');
// Triggers updates for all signals with keys starting with 'user/'
// - userProfile signal updates
// - userSettings signal updates  
// - userPreferences signal updates
// All automatically and efficiently!

// Save multiple related items
await storage.put('user/profile', profile);
await storage.put('user/settings', settings);  
await storage.put('user/preferences', preferences);
// Each triggers individual signal updates

Batch Event Handling

Handle multiple storage changes efficiently:

let pendingUpdates = new Set<string>();
let updateTimer: any;

storage.onEvent((event) => {
  if (event.key) {
    pendingUpdates.add(event.key);
  }
  
  // Debounce updates
  clearTimeout(updateTimer);
  updateTimer = setTimeout(() => {
    console.log('Batch update for keys:', Array.from(pendingUpdates));
    
    // Process all pending updates
    processBatchUpdate(Array.from(pendingUpdates));
    pendingUpdates.clear();
  }, 100);
});

📊 Performance Optimization

Signal Performance

Signals are highly optimized for performance:

// ✅ GOOD: Signals only update when data actually changes
const userProfile = storage.asSignal<UserProfile>('user/profile', null).value;

// Even if you call put with the same data multiple times:
await storage.put('user/profile', sameProfileData);
await storage.put('user/profile', sameProfileData);
await storage.put('user/profile', sameProfileData);
// Signal only updates once, UI only re-renders once

Memory Management

Signals automatically clean up when components are destroyed:

@Component({...})
export class MyComponent implements OnDestroy {
  // Signals automatically clean up on component destruction
  userSettings = inject(StorageService).asSignal<Settings>('user/settings', {}).value;
  
  ngOnDestroy() {
    // No manual cleanup needed - signals handle this automatically
  }
}

🌐 Real-World Use Cases

PWA Offline Support

Signals work seamlessly offline and online:

@Component({
  template: `
    <div class="sync-status">
      @if (isOnline()) {
        ✅ Online - Data syncing
      } @else {
        ⏳ Offline - Changes saved locally
      }
    </div>
    <div>{{ userData() | json }}</div>
  `
})
export class SyncStatusComponent {
  private storage = inject(StorageService);
  
  userData = this.storage.asSignal<any>('user/data', {}).value;
  isOnline = signal(navigator.onLine);

  constructor() {
    // Update online status
    window.addEventListener('online', () => this.isOnline.set(true));
    window.addEventListener('offline', () => this.isOnline.set(false));
  }
}

Real-Time Collaboration

Combine with MongoDB for real-time multi-user features:

@Component({
  template: `
    <div class="document">
      <h1>{{ document().title }}</h1>
      <p>Last edited by: {{ document().lastEditedBy }}</p>
      <textarea [value]="document().content" 
                (input)="updateContent($any($event.target).value)">
      </textarea>
    </div>
  `
})
export class CollaborativeDocComponent {
  // Real-time signal with MongoDB sync
  document = autoFlashState({
    key: 'current-doc',
    defaultValue: { title: '', content: '', lastEditedBy: '' },
    database: 'documents',
    collection: 'live-docs',
    priority: 'realtime' // MongoDB Change Streams
  });

  updateContent(content: string) {
    this.document.update(doc => ({
      ...doc,
      content,
      lastEditedBy: this.currentUser
    }));
    // Automatically syncs to all users via MongoDB!
  }
}

🚀 Best Practices

1. Use Semantic Signal Names

// ✅ GOOD: Descriptive names
readonly userProfile = storage.asSignal<UserProfile>('user/profile', null).value;
readonly appSettings = storage.asSignal<AppSettings>('app/settings', {}).value;

// ❌ AVOID: Generic names
readonly data1 = storage.asSignal<any>('data1', null).value;
readonly stuff = storage.asSignal<any>('stuff', null).value;

2. Provide Sensible Defaults

// ✅ GOOD: Provide defaults that make sense
readonly userSettings = storage.asSignal<UserSettings>('user/settings', {
  theme: 'light',
  notifications: true,
  language: 'en'
}).value;

// ❌ AVOID: No defaults or inappropriate defaults
readonly userSettings = storage.asSignal<UserSettings>('user/settings', null).value;

3. Use Computed Signals for Derived State

// ✅ GOOD: Derive state from storage signals
readonly isDarkMode = computed(() => this.userSettings().theme === 'dark');
readonly isAdmin = computed(() => this.userProfile()?.role === 'admin');

// ❌ AVOID: Storing derived state in storage
await storage.put('user/isDarkMode', userSettings.theme === 'dark'); // Redundant

4. Batch Related Updates

// ✅ GOOD: Batch related operations
async updateUserProfile(updates: Partial<UserProfile>) {
  const current = await storage.get<UserProfile>('user/profile');
  await storage.put('user/profile', { ...current, ...updates });
}

// ❌ AVOID: Multiple individual updates
await storage.put('user/name', newName);
await storage.put('user/email', newEmail);
await storage.put('user/phone', newPhone);

📚 Related Documentation


The events and signals system provides the foundation for building truly responsive, real-time applications with automatic synchronization! 🚀