Skip to content

Rearchitect Domain Events to Support source_client_id Filtering #665

Description

@malibio

Overview

The current domain event architecture emits events from SurrealStore (data layer) without any client identification, causing feedback loops when Tauri receives its own events back during optimistic UI updates. This needs to be rearchitected so events are emitted at the NodeService layer with source_client_id awareness.

Problem Statement

Current Architecture (Broken):

  1. Frontend performs optimistic UI update
  2. Tauri command calls NodeServiceSurrealStore
  3. SurrealStore.emit_event() broadcasts DomainEvent (no client_id)
  4. DomainEventForwarder receives event and forwards to Tauri frontend
  5. Frontend receives its own change back → state corruption/race conditions

Evidence:

Root Cause:

  • emit_event() is called directly in SurrealStore (data layer) with no client context
  • No mechanism to pass client_id through the call chain
  • DomainEventForwarder forwards ALL events blindly

Proposed Solution

Move event emission to the NodeService layer with client_id awareness:

Target Architecture:

  1. Each client (Tauri, MCP, proxy, cloud sync) has a unique client_id
  2. NodeService methods accept optional client_id parameter (or use context)
  3. Events are emitted at NodeService level with source_client_id
  4. DomainEventForwarder filters events matching subscriber's client_id

Key Design Decision:

  • NodeService should be the shared service layer for ALL clients
  • Data layer (SurrealStore) remains unaware of clients
  • All entry points (Tauri commands, MCP handlers, HTTP proxy) go through NodeService

Implementation Approach

Phase 1: Refactor Event Emission Location

  • Remove emit_event() calls from SurrealStore
  • Add event emission to NodeService after successful operations
  • Keep SurrealStore.event_tx broadcast channel for now (or move to NodeService)

Phase 2: Add Client Context

  • Create ClientContext type with client_id: String
  • Add with_client() or similar method to NodeService for setting context
  • Update DomainEvent struct to include source_client_id: Option<String>

Phase 3: Update Event Consumers

  • Update DomainEventForwarder to accept Tauri's client_id
  • Filter events where source_client_id == subscriber_client_id
  • Re-enable DomainEventForwarder in db.rs

Phase 4: Fix Layering Violations

  • Audit Tauri commands for direct SurrealStore calls
  • Route all operations through NodeService
  • get_all_edges in nodes.rs:890 is only current violation (read-only, low priority)

Acceptance Criteria

  • Events emitted at NodeService layer, not SurrealStore
  • DomainEvent includes source_client_id: Option<String>
  • Tauri frontend does not receive its own events back
  • MCP client events are properly identified and filterable
  • Rapid Enter + Shift+Tab no longer causes "Sibling not found" errors
  • Browser mode (dev-proxy.rs) continues to work
  • Code passes bun run quality:fix
  • Tests pass (bun run test:all)

Technical Specifications

Reference Files

  • SurrealStore: packages/core/src/db/surreal_store.rs - Current event emission location
  • DomainEvent: packages/core/src/db/events.rs - Event types (needs source_client_id)
  • NodeService: packages/core/src/services/node_service.rs - Target event emission location
  • DomainEventForwarder: packages/desktop-app/src-tauri/src/services/domain_event_forwarder.rs - Needs filtering
  • db.rs: packages/desktop-app/src-tauri/src/commands/db.rs - DomainEventForwarder currently disabled
  • dev-proxy.rs: packages/desktop-app/src-tauri/src/bin/dev-proxy.rs - Working client_id example

Current emit_event Locations in SurrealStore

Lines: 975, 1206, 1207, 1443, 1615, 1722, 1753, 1840, 2943, 2948, 3010, 3031

Browser Mode Pattern (Reference)

// dev-proxy.rs has working client_id filtering
pub enum SseEvent {
    NodeCreated {
        node_id: String,
        node_data: Node,
        client_id: Option<String>,  // ← This is what we need
    },
    // ...
}

// Filter logic
if let (Some(this_client), Some(event_client)) = (&client_id, event_client_id) {
    if this_client == event_client {
        return None; // Filter out own events
    }
}

Dependencies

Related Issues

Non-Goals

  • Changing the broadcast channel mechanism (tokio broadcast is fine)
  • Adding client_id to every database operation signature
  • Real-time collaborative editing (future scope)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions