Skip to content
Documentation GitHub
Platform

Write-Effect Coordinator

Status: Implemented Crate: apps/desktop/src-tauri/src/side_effects.rs


The Write-Effect Coordinator is a static namespace struct (WriteEffectCoordinator) that dispatches fire-and-forget side effects after every write operation. It exists to enforce a critical invariant: writes never fail due to side-effect failures. Analytics tracking, event logging, and embedding re-indexing are all best-effort — if any side effect fails, the error is logged and discarded.

This decoupling serves three purposes:

  1. Write reliability — A page save must never fail because analytics is unreachable or the embedding model is unavailable. Side effects are secondary to the user’s data mutation.

  2. Centralized audit — Every write path in the application flows through one coordinator, making it easy to audit which mutations trigger which side effects.

  3. Zero-cost abstraction — The coordinator is a static struct with no instance state. Methods take &AppState as a parameter, avoiding circular ownership in the dependency graph.

Each public method on WriteEffectCoordinator corresponds to a specific mutation. The table below lists all methods grouped by entity domain, along with which side effects each triggers.

MethodEntity TypeEvent TypeAnalyticsEvent LogEmbedding
on_page_createdPageCreatedYesYes
on_page_updatedPageUpdatedYes
on_page_deletedPageDeletedYesYes
on_page_movedPageMovedYes
on_page_renamedPageRenamedYes
MethodEntity TypeEvent TypeAnalyticsEvent LogEmbedding
on_block_content_savedBlockUpdatedYesYes
on_block_metadata_updatedBlockUpdatedYesYes
on_block_area_changedBlockUpdatedYesYes
MethodEntity TypeEvent TypeAnalyticsEvent LogEmbedding
on_workspace_createdYes
on_workspace_openedYes
MethodEntity TypeEvent TypeAnalyticsEvent LogEmbedding
on_layout_changed

Layout changes are currently a no-op placeholder.

MethodEntity TypeEvent TypeAnalyticsEvent LogEmbedding
on_tag_createdTagCreatedYes
on_tag_updatedTagUpdatedYes
on_tag_deletedTagDeletedYes
on_tag_assigned_to_pagePageTagAssignedYes
on_tag_removed_from_pagePageTagRemovedYes
on_page_tags_replacedPageTagReplacedYes
MethodEntity TypeEvent TypeAnalyticsEvent LogEmbedding
on_attachment_uploadedAttachmentCreatedYes
on_attachment_deletedAttachmentDeletedYes
MethodEntity TypeEvent TypeAnalyticsEvent LogEmbedding
on_type_createdTypeDefinitionCreatedYes
on_type_updatedTypeDefinitionUpdatedYes
on_type_deletedTypeDefinitionDeletedYes
MethodEntity TypeEvent TypeAnalyticsEvent LogEmbedding
on_property_createdPropertyCreatedYes
on_property_updatedPropertyUpdatedYes
on_property_deletedPropertyDeletedYes

Calls crate::analytics::track_event(state, event_name, None). Fire-and-forget with log-discard on error. Currently triggered only by page create/delete and workspace create/open events.

Calls RecordEventUseCase::execute() with entity type, entity ID, event type, and optional before/after values. The use case writes to the event_log SQLite table. Fire-and-forget — the Result is discarded with let _ =.

Sends an EmbeddingEvent::EmbedPage { page_id } to the EmbeddingTask via a bounded channel (try_send with capacity 256). If the channel is full, the event is silently dropped. Triggered only by block content mutations (on_block_content_saved, on_block_metadata_updated, on_block_area_changed).

The coordinator enforces a strict invariant: write operations are never rolled back due to side-effect failures.

  • Analytics: track_event handles its own errors internally. The coordinator does not inspect the result.
  • Event Log: RecordEventUseCase returns a Result that is explicitly discarded (let _ = use_case.execute(...)). The use case logs errors internally.
  • Embedding: try_send returns Err if the channel is full. The error is discarded with let _ =. The page is re-indexed on the next workspace open via IndexWorkspace.

The embedding side effect is backed by EmbeddingTask, which implements the EventDrivenTask trait from infrastructure-task-runner.

Key behaviors:

  • Bounded channel: Capacity 256 events. Overflow is silently dropped (backpressure via try_send).
  • Deduplication: Batched events are collected into a HashSet<Uuid> before processing, eliminating duplicate page IDs within a batch.
  • Debounce: 2-second delay (DEBOUNCE_DELAY) before processing, allowing rapid successive saves to coalesce.
  • Blocking execution: Embedding computation runs on tokio::task::spawn_blocking to avoid blocking the async runtime.
  • Status broadcasting: Bulk indexing progress is published via a watch channel as IndexingStatus (Idle or Indexing { completed, total }).
  • Two event types: EmbedPage (single page re-embed after save) and IndexWorkspace (bulk index on workspace open or model upgrade).
FileDescription
apps/desktop/src-tauri/src/side_effects.rsWriteEffectCoordinator struct (all public methods)
apps/desktop/src-tauri/src/embedding.rsEmbeddingTask (EventDrivenTask impl, debounce, batching)
crates/application/src/event_log/record.rsRecordEventUseCase (event log persistence)
crates/domain/src/event_log.rsEntityType, EventType domain enums
crates/infrastructure/task-runner/src/lib.rsEventDrivenTask trait and TaskRunner
apps/desktop/src-tauri/src/commands/Tauri command handlers that call coordinator methods

Was this page helpful?