docs/core/events.mdx
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.
The event bus enables reactive UI updates by notifying clients when data changes. The system uses:
ResourceChanged) handles all database entitiesGeneric events that work for ALL resources (files, tags, albums, locations, etc.):
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 tagscollection - File collectionslocation - Indexed locationsdevice - Devices in the networkvolume - Storage volumes (replaces deprecated volume events)sidecar - Generated thumbnails and metadatauser_metadata - User-added metadata (notes, favorites, etc.)content_identity - Deduplicated content recordsSpecialized events for system operations:
Core Lifecycle:
CoreStarted, CoreShutdown - Daemon lifecycleLibrary Management:
LibraryCreated, LibraryOpened, LibraryClosed, LibraryDeletedRefresh - Invalidate all frontend cachesJobs:
JobQueued, JobStarted, JobProgress, JobCompleted, JobFailed, JobCancelledSync:
SyncStateChanged - Sync state transitionsSyncActivity - Peer sync activitySyncConnectionChanged - Peer connectionsSyncError - Sync errorsVolumes (deprecated - use ResourceChanged with resource_type: "volume"):
VolumeAdded, VolumeRemoved, VolumeUpdatedVolumeMountChanged, VolumeSpeedTestedIndexing (deprecated - use job events):
IndexingStarted, IndexingProgress, IndexingCompleted, IndexingFailedFilesystem:
FsRawChange - Raw filesystem watcher events (before database resolution)Events are emitted automatically when using the TransactionManager:
// 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:
Only use manual emission for infrastructure events:
// Jobs, sync, and system events
event_bus.emit(Event::JobStarted {
job_id: job.id.to_string(),
job_type: "IndexLocation".to_string(),
});
Subscribe to events affecting specific directories or files:
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:
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:
The useNormalizedQuery hook automatically subscribes to resource events and updates the cache:
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:
ResourceChanged events matching the resource type// 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
}
}
}
Monitor events in real-time using the CLI:
# 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 outputExample 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)
Event enum: core/src/infra/event/mod.rs
#[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
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);
}
useNormalizedQuery handles subscriptions automaticallyevent_bus.emit() for resources