---
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
