Back to Spacedrive

Event System

docs/core/events.mdx

0.4.310.1 KB
Original Source

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

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