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
- BroadcastChannel: Uses
BroadcastChannel('gg-storage:<namespace>')for communication - Namespace Isolation: Each namespace gets its own channel for perfect isolation
- Event Propagation: Changes broadcast to all tabs automatically
- 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
- API Reference - Complete signal API
- Flash Persistence - Auto-persistent signals
- Integration Guide - Setup and patterns
- Performance Guide - Optimization strategies
The events and signals system provides the foundation for building truly responsive, real-time applications with automatic synchronization! 🚀