LogseqSpringThing uses the Actix actor system to manage concurrent state and provide a message-passing architecture. This replaces traditional Arc<RwLock<T>> patterns with actors that encapsulate state and handle messages asynchronously.
The actor system consists of six primary actors that work together to manage the application state:
graph TB
CM[ClientManagerActor] --> GS[GraphServiceActor]
CM --> GPU[GPUComputeActor]
GS --> GPU
GS --> MA[MetadataActor]
SA[SettingsActor] --> GS
PSA[ProtectedSettingsActor] --> SA
style CM fill:#3A3F47,stroke:#61DAFB,color:#FFFFFF
style GS fill:#3A3F47,stroke:#61DAFB,color:#FFFFFF
style GPU fill:#3A3F47,stroke:#61DAFB,color:#FFFFFF
style MA fill:#3A3F47,stroke:#61DAFB,color:#FFFFFF
style SA fill:#3A3F47,stroke:#61DAFB,color:#FFFFFF
style PSA fill:#3A3F47,stroke:#61DAFB,color:#FFFFFF
Location: src/actors/graph_actor.rs
The central actor managing the graph data structure, including nodes and edges.
Responsibilities:
- Maintains the graph data structure with nodes and edges
- Manages node positions and velocities
- Coordinates with GPU compute for physics simulation
- Handles graph modifications (add/remove nodes and edges)
- Broadcasts updates to connected clients
Key Messages:
GetGraphData- Retrieve the complete graphUpdateGraphData- Update the entire graph structureUpdateNodePosition- Update a single node's positionSimulationStep- Trigger a single physics simulation stepAddNode/RemoveNode- Modify nodesAddEdge/RemoveEdge- Modify edgesBuildGraphFromMetadata- Initialize graph from metadata
Location: src/actors/client_manager_actor.rs
Manages WebSocket client connections and broadcasts.
Responsibilities:
- Tracks connected WebSocket clients
- Broadcasts graph updates to all clients
- Manages client registration/unregistration
- Handles binary protocol encoding for efficient updates
Key Messages:
RegisterClient- Register a new WebSocket clientUnregisterClient- Remove a disconnected clientBroadcastNodePositions- Send binary node updates to all clientsBroadcastMessage- Send text messages to all clientsGetClientCount- Get number of connected clients
Location: src/actors/gpu_compute_actor.rs
Manages GPU-accelerated physics computations with CPU fallback.
Responsibilities:
- Initializes CUDA context for GPU computation
- Computes force-directed graph layout physics
- Falls back to CPU computation if GPU unavailable
- Manages node position updates from physics simulation
Key Messages:
InitializeGPU- Initialize with graph dataUpdateGPUGraphData- Update graph structureUpdateSimulationParams- Modify physics parametersComputeForces- Run one physics iterationGetNodeData- Retrieve computed positionsGetGPUStatus- Check GPU/CPU status
Location: src/actors/settings_actor.rs
Manages application settings and configuration.
Responsibilities:
- Stores and retrieves application settings
- Handles settings updates from clients
- Provides path-based setting access
- Integrates with file-based settings persistence
Key Messages:
GetSettings- Retrieve all settingsUpdateSettings- Update complete settingsGetSettingByPath- Get specific setting by pathSetSettingByPath- Update specific setting by path
Location: src/actors/metadata_actor.rs
Manages graph metadata and layout information.
Responsibilities:
- Loads metadata from JSON files
- Provides metadata for graph construction
- Handles metadata refresh operations
- Stores and manages node and edge metadata (via
MetadataStorewhich is aHashMap)
Key Messages:
GetMetadata- Retrieve complete metadataUpdateMetadata- Update metadata storeRefreshMetadata- Reload metadata from disk
Location: src/actors/protected_settings_actor.rs
Manages protected settings like API keys.
Responsibilities:
- Stores sensitive configuration separately
- Provides controlled access to API keys
- Handles user-specific protected settings
- Integrates with authentication system
Key Messages:
GetApiKeys- Retrieve API keys for a specific userValidateClientToken- Validate a client's session tokenStoreClientToken- Store a new client session tokenUpdateUserApiKeys- Update a user's API keysCleanupExpiredTokens- Trigger cleanup of expired session tokensMergeSettings- Merge new settings into protected settingsSaveSettings- Persist protected settings to fileGetUser- Retrieve a specific user's protected data
sequenceDiagram
participant Client
participant WebSocket
participant ClientManagerActor
participant GraphServiceActor
participant GPUComputeActor
Client->>WebSocket: Connect
WebSocket->>ClientManagerActor: RegisterClient
ClientManagerActor-->>WebSocket: client_id
loop Simulation Loop
GraphServiceActor->>GPUComputeActor: ComputeForces
GPUComputeActor-->>GraphServiceActor: positions
GraphServiceActor->>ClientManagerActor: BroadcastNodePositions
ClientManagerActor->>WebSocket: Binary update
WebSocket->>Client: Node positions
end
sequenceDiagram
participant Client
participant Handler
participant SettingsActor
participant ProtectedSettingsActor
Client->>Handler: Update setting
Handler->>SettingsActor: SetSettingByPath
alt Protected setting
SettingsActor->>ProtectedSettingsActor: Update protected
ProtectedSettingsActor-->>SettingsActor: OK
end
SettingsActor-->>Handler: Success
Handler-->>Client: Updated
Actors are typically initialized in the application state setup:
// In app_state.rs
let client_manager = ClientManagerActor::new().start();
let gpu_compute = GPUComputeActor::new(/* params */).start();
let graph_service = GraphServiceActor::new(
client_manager.clone(),
Some(gpu_compute.clone())
).start();Each actor implements handlers for its messages:
impl Handler<GetGraphData> for GraphServiceActor {
type Result = Result<GraphData, String>;
fn handle(&mut self, _: GetGraphData, _: &mut Self::Context) -> Self::Result {
Ok(self.get_graph_data().clone())
}
}Actors communicate asynchronously using addresses:
// Send message and await response
let graph_data = graph_actor.send(GetGraphData).await??;
// Send without waiting
graph_actor.do_send(StartSimulation);- Message Design: Keep messages small and focused on single operations
- Error Handling: All actor messages return
Result<T, String>for consistent error handling - State Encapsulation: Actors own their state; no shared mutable state
- Async Operations: Use
AsyncContextfor long-running operations - Supervision: Actors automatically restart on failure
- The actor system eliminates lock contention from
Arc<RwLock<T>> - Messages are processed sequentially within each actor
- Use
do_sendfor fire-and-forget operations - Binary protocol reduces serialization overhead
- GPU computation offloads expensive physics calculations
Actors can be tested in isolation:
#[actix_rt::test]
async fn test_graph_actor() {
let client_manager = ClientManagerActor::new().start();
let actor = GraphServiceActor::new(client_manager, None).start();
let result = actor.send(GetGraphData).await.unwrap();
assert!(result.is_ok());
}