spacedrive/docs/core/events.mdx
2025-12-02 05:52:07 -08:00

349 lines
10 KiB
Plaintext

---
title: Event System
sidebarTitle: Events
---
Spacedrive's event system broadcasts real-time updates to all connected clients using a **unified resource event architecture** that eliminates per-resource event variants in favor of generic, horizontally-scalable events.
## Overview
The event bus enables reactive UI updates by notifying clients when data changes. The system uses:
- **Generic Resource Events**: A single event type (`ResourceChanged`) handles all database entities
- **Path-Scoped Subscriptions**: Subscribe to events affecting specific directories or files
- **Infrastructure Events**: Specialized events for jobs, sync, and system lifecycle
- **Automatic Emission**: Events are emitted automatically by the TransactionManager - no manual calls needed
## Event Types
### Resource Events
Generic events that work for ALL resources (files, tags, albums, locations, etc.):
```rust
Event::ResourceChanged {
resource_type: String, // e.g., "file", "tag", "album", "location"
resource: serde_json::Value, // Full resource data as JSON
metadata: Option<ResourceMetadata>, // Cache hints and path scopes
}
Event::ResourceChangedBatch {
resource_type: String,
resources: serde_json::Value, // Array of resources
metadata: Option<ResourceMetadata>,
}
Event::ResourceDeleted {
resource_type: String,
resource_id: Uuid,
}
```
**Supported Resources**:
- `file` - Files and directories (Entry entity)
- `tag` - User tags
- `collection` - File collections
- `location` - Indexed locations
- `device` - Devices in the network
- `volume` - Storage volumes (replaces deprecated volume events)
- `sidecar` - Generated thumbnails and metadata
- `user_metadata` - User-added metadata (notes, favorites, etc.)
- `content_identity` - Deduplicated content records
<Note>
Volume events (`VolumeAdded`, `VolumeUpdated`, etc.) and indexing events (`IndexingStarted`, `IndexingProgress`, etc.) are deprecated. Use `ResourceChanged` for volumes and job events for indexing progress.
</Note>
### Infrastructure Events
Specialized events for system operations:
**Core Lifecycle**:
- `CoreStarted`, `CoreShutdown` - Daemon lifecycle
**Library Management**:
- `LibraryCreated`, `LibraryOpened`, `LibraryClosed`, `LibraryDeleted`
- `Refresh` - Invalidate all frontend caches
**Jobs**:
- `JobQueued`, `JobStarted`, `JobProgress`, `JobCompleted`, `JobFailed`, `JobCancelled`
**Sync**:
- `SyncStateChanged` - Sync state transitions
- `SyncActivity` - Peer sync activity
- `SyncConnectionChanged` - Peer connections
- `SyncError` - Sync errors
**Volumes** (deprecated - use `ResourceChanged` with `resource_type: "volume"`):
- ~~`VolumeAdded`, `VolumeRemoved`, `VolumeUpdated`~~
- ~~`VolumeMountChanged`, `VolumeSpeedTested`~~
**Indexing** (deprecated - use job events):
- ~~`IndexingStarted`, `IndexingProgress`, `IndexingCompleted`, `IndexingFailed`~~
**Filesystem**:
- `FsRawChange` - Raw filesystem watcher events (before database resolution)
## Event Emission
### Automatic Emission (Recommended)
Events are emitted automatically when using the TransactionManager:
```rust
// NO manual event emission needed!
pub async fn create_collection(
tm: &TransactionManager,
library: Arc<Library>,
name: String,
) -> Result<Collection> {
let model = collection::ActiveModel {
id: NotSet,
uuid: Set(Uuid::new_v4()),
name: Set(name),
// ...
};
// TM handles: DB write + sync log + event emission
let collection = tm.commit::<collection::Model, Collection>(library, model).await?;
Ok(collection) // ResourceChanged event already emitted!
}
```
The TransactionManager emits `ResourceChanged` after successful commits, ensuring:
- ✅ Events always match database state
- ✅ No forgotten emissions
- ✅ Automatic sync log integration
### Manual Emission (Infrastructure Only)
Only use manual emission for infrastructure events:
```rust
// Jobs, sync, and system events
event_bus.emit(Event::JobStarted {
job_id: job.id.to_string(),
job_type: "IndexLocation".to_string(),
});
```
## Path-Scoped Subscriptions
Subscribe to events affecting specific directories or files:
```rust
use sd_core::infra::event::SubscriptionFilter;
// Subscribe to changes in a specific directory
let filter = SubscriptionFilter::PathScoped {
resource_type: "file".to_string(),
path_scope: SdPath::physical(device_slug, "/Users/james/Photos"),
};
let mut subscriber = event_bus.subscribe_filtered(vec![filter]);
while let Ok(event) = subscriber.recv().await {
// Only receives events affecting /Users/james/Photos
println!("Event: {:?}", event);
}
```
The `ResourceMetadata` field includes `affected_paths` that indicate which directories/files changed:
```rust
pub struct ResourceMetadata {
pub no_merge_fields: Vec<String>, // Fields to replace, not merge
pub alternate_ids: Vec<Uuid>, // Alternate IDs for matching
pub affected_paths: Vec<SdPath>, // Paths affected by this event
}
```
Path matching supports:
- **Physical paths**: Match by device slug + path prefix
- **Content IDs**: Match by content identifier
- **Cloud paths**: Match by service + bucket + path
- **Sidecar paths**: Match by content ID
## Client Integration
### TypeScript (useNormalizedQuery)
The `useNormalizedQuery` hook automatically subscribes to resource events and updates the cache:
```typescript
import { useNormalizedQuery } from '@sd/client';
// Automatically subscribes to ResourceChanged events for "tag"
const tags = useNormalizedQuery({
resource_type: 'tag',
query: api.tags.list(),
});
// UI automatically updates when tags change!
```
The normalized cache:
1. Subscribes to `ResourceChanged` events matching the resource type
2. Deserializes the JSON resource using generated TypeScript types
3. Updates the local cache
4. Triggers React re-renders
### Swift
```swift
// Generic event handler works for ALL resources
actor EventCacheUpdater {
let cache: NormalizedCache
func handleEvent(_ event: Event) async {
switch event.kind {
case .ResourceChanged(let resourceType, let resourceJSON):
// Generic decode via type registry
let resource = try ResourceTypeRegistry.decode(
resourceType: resourceType,
from: resourceJSON
)
await cache.updateEntity(resource)
case .ResourceDeleted(let resourceType, let resourceId):
await cache.deleteEntity(resourceType: resourceType, id: resourceId)
default:
break
}
}
}
```
## CLI Event Monitoring
Monitor events in real-time using the CLI:
```bash
# Monitor all events
sd events monitor
# Filter by event type
sd events monitor --event-type JobProgress,JobCompleted
# Filter by library
sd events monitor --library-id <uuid>
# Filter by job
sd events monitor --job-id <id>
# Show timestamps
sd events monitor --timestamps
# Verbose mode (full JSON)
sd events monitor --verbose --pretty
```
**Available Filters**:
- `-t, --event-type` - Comma-separated event types (e.g., `ResourceChanged,JobProgress`)
- `-l, --library-id` - Filter by library UUID
- `-j, --job-id` - Filter by job ID
- `-d, --device-id` - Filter by device UUID
- `--timestamps` - Show event timestamps
- `-v, --verbose` - Show full event JSON
- `-p, --pretty` - Pretty-print JSON output
**Example Output**:
```
Monitoring events - Press Ctrl+C to exit
═══════════════════════════════════════════════════════
Connected to event stream
JobStarted: Job started: IndexLocation (a1b2c3d4)
JobProgress: Job progress: IndexLocation (a1b2c3d4) - 45.2% - Scanning directory
ResourceChangedBatch: Resources changed: file (127 items)
JobCompleted: Job completed: IndexLocation (a1b2c3d4)
```
## Implementation Reference
**Event enum**: `core/src/infra/event/mod.rs`
```rust
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
pub enum Event {
// Core lifecycle
CoreStarted,
CoreShutdown,
// Library events
LibraryOpened { id: Uuid, name: String, path: PathBuf },
LibraryClosed { id: Uuid, name: String },
// Generic resource events
ResourceChanged {
resource_type: String,
resource: serde_json::Value,
metadata: Option<ResourceMetadata>,
},
ResourceChangedBatch {
resource_type: String,
resources: serde_json::Value,
metadata: Option<ResourceMetadata>,
},
ResourceDeleted {
resource_type: String,
resource_id: Uuid,
},
// Jobs, sync, volumes, indexing...
// (See full enum in source)
}
```
**Event bus**: `core/src/infra/event/mod.rs`
```rust
pub struct EventBus {
sender: broadcast::Sender<Event>,
subscribers: Arc<RwLock<Vec<FilteredSubscriber>>>,
}
impl EventBus {
// Subscribe to all events
pub fn subscribe(&self) -> EventSubscriber;
// Subscribe with path/resource filters
pub fn subscribe_filtered(&self, filters: Vec<SubscriptionFilter>) -> EventSubscriber;
// Emit an event
pub fn emit(&self, event: Event);
}
```
## Benefits
### Backend
- **Zero Manual Emission**: TransactionManager handles all resource events
- **Type Safety**: Events always match actual resources
- **Centralized**: Single point of emission prevents drift
- **Scalable**: Adding new resources requires no event code
### Frontend
- **Zero Boilerplate**: One event handler for all resource types
- **Type Registry**: Automatic deserialization via generated types
- **Path Scoping**: Subscribe only to relevant directory changes
- **Cache Integration**: `useNormalizedQuery` handles subscriptions automatically
### Developer Experience
- **No Event Variants**: ~40 variants eliminated → 3 generic events
- **No Manual Calls**: Never call `event_bus.emit()` for resources
- **No Client Changes**: Adding a 100th resource type = zero event handling updates
- **CLI Debugging**: Monitor events in real-time with filtering
## Related Documentation
- **Sync System**: See [sync.md](./sync.md) for event emission during sync
- **Normalized Cache**: See [normalized_cache.md](./normalized_cache.md) for client-side event handling
- **TransactionManager**: See [transactions.md](./transactions.md) for automatic event emission