NodeManager Architecture Refactoring
Complete refactoring of NodeManager from monolithic implementation into clean architecture with separate ServerNodeManager and ClientNodeManager optimized for their specific use cases
NodeManager Refactoring Summary
Overview
Refactored NodeManager from a single monolithic implementation into a clean architecture with:
- Base class with common functionality
- ServerNodeManager optimized for data processing and cluster management
- ClientNodeManager optimized for driving Compose UI
- DefaultNodeManager wrapper for backward compatibility
Architecture
BaseNodeManager (Abstract Base Class)
Location: /krill-sdk/src/commonMain/kotlin/krill/zone/node/NodeManager.kt
Contains common functionality:
- State transition methods (execute, idle, complete, running, alarm, error)
- Node queries (nodes, readNode, children, getUpstreamDataPoint)
- Helper methods (findServer, executeChildren, observe)
- Shared node map and swarm StateFlow
Protected Members:
nodes: MutableMap<String, NodeFlow>- Shared node storage_swarm: MutableStateFlow<Set<String>>- Reactive swarm updatesobserver: NodeObserver- Node observation managementnodeHttp: NodeHttp- HTTP client for inter-server communication
ServerNodeManager
Purpose: Maintains full network cluster state with thread safety
Key Features:
- Thread-Safe Actor Pattern
- Uses
Channel<NodeOperation>for serialized updates - FIFO queue ensures ordered processing
- Prevents race conditions from multiple threads
- Uses
- Persistent Storage
- All nodes persisted to disk via FileOperations
- Survives server restarts
- Loads stored nodes on initialization
- Selective Observation
- Only observes nodes that belong to this server (
node.isMine()) - Reduces processing overhead
- Other nodes maintained in map but not actively processed
- Only observes nodes that belong to this server (
- Cluster Management
- Tracks all nodes in the entire network
- Handles server disconnections gracefully
- Maintains parent-child relationships across servers
Implementation Details:
1
2
3
4
5
6
7
8
class ServerNodeManager(
observer: NodeObserver,
nodeHttp: NodeHttp,
private val fileOperations: FileOperations,
installId: String,
private val hostName: String,
scope: CoroutineScope
) : BaseNodeManager(observer, nodeHttp, installId, scope)
Actor Operations:
Update- Add/modify nodes with automatic observationDelete- Remove nodes and cascade to childrenRemove- Fast removal without HTTP calls
ClientNodeManager
Purpose: Optimized for Compose UI reactivity
Key Features:
- Simplified Update Logic
- No actor pattern needed (single-threaded UI context)
- Direct node map mutations
- Immediate swarm updates for UI reactivity
- No File Operations
- Nodes downloaded from servers via HTTP/WebSocket
- User edits posted to server
- Lightweight and fast
- Universal Observation
- Observes ALL nodes for UI updates
- Drives Compose recomposition
- Updates swarm StateFlow for reactive UI
- Delegation to Server
- Deletes sent to server via HTTP
- Server handles actual deletion and persistence
- Local removal immediate for UI feedback
Implementation Details:
1
2
3
4
5
6
class ClientNodeManager(
observer: NodeObserver,
nodeHttp: NodeHttp,
installId: String,
scope: CoroutineScope
) : BaseNodeManager(observer, nodeHttp, installId, scope)
Optimizations:
- Direct map access (no channel overhead)
- Immediate swarm updates
- Async HTTP for user edits
- Simple shutdown
DefaultNodeManager (Compatibility Wrapper)
Purpose: Maintains backward compatibility with existing code
Implementation:
1
2
3
4
5
class DefaultNodeManager(...) : NodeManager by if (SystemInfo.isServer()) {
ServerNodeManager(...)
} else {
ClientNodeManager(...)
}
Delegates all methods to appropriate implementation based on runtime environment.
Key Improvements
1. Separation of Concerns
- Server: Focus on data integrity, persistence, cluster management
- Client: Focus on UI reactivity, user interactions
- No more
if (SystemInfo.isServer())checks scattered throughout
2. Performance Optimizations
- Server: Thread-safe actor pattern prevents race conditions
- Client: Direct mutations for faster UI updates
- Selective observation reduces unnecessary processing
3. Code Clarity
- Clear purpose for each class
- Reduced complexity in each implementation
- Base class consolidates common behavior
4. Maintainability
- Easier to reason about server vs client behavior
- Changes to one don’t affect the other
- Testing can be done independently
Observation Strategy
Server (ServerNodeManager)
1
2
3
4
5
6
7
override suspend fun observeNode(node: Node) {
// Server only observes nodes that belong to it
if (node.isMine()) {
val flow = readNode(node.id)
observe(flow)
}
}
Why: Servers process thousands of nodes from multiple servers. Only nodes owned by this server need active processing.
Client (ClientNodeManager)
1
2
3
4
5
override suspend fun observeNode(node: Node) {
// Client observes all nodes for UI
val flow = readNode(node.id)
observe(flow)
}
Why: Clients display a curated subset of nodes. All visible nodes need observation for Compose reactivity.
Node Lifecycle
On Server
graph TD
A[Node Created] --> B[Saved to Disk]
B --> C[Added to Map]
C --> D{isMine?}
D -->|Yes| E[Observed]
D -->|No| F[Stored Only]
E --> G[Processors React]
H[Node Updated] --> I[Disk Updated]
I --> J[StateFlow Emits]
J --> D
K[Node Deleted] --> L[Remove from Disk]
L --> M[Children Cascaded]
M --> N[Cleanup]
On Client
- Node received (HTTP/WS) → Added to map → Observed → Swarm updated → UI recomposes
- User edits → Posted to server → Local optimistic update
- Server update received → StateFlow emits → UI recomposes
Disconnection Handling
Server
1
2
3
4
5
override suspend fun onDisconnect(wire: NodeWire) {
// Find disconnected server and mark as ERROR
// Keep child nodes visible with ERROR parent
// Will refresh when server reconnects with new session
}
Client
1
2
3
4
override suspend fun onDisconnect(wire: NodeWire) {
// Mark disconnected server as ERROR
// UI shows connection lost
}
Testing Results
✅ Server compiles successfully
✅ Server starts without errors
✅ Actor pattern processes updates correctly
✅ Nodes persist to disk
✅ Selective observation working (only isMine() nodes observed)
✅ Beacon discovery and handshake functional
✅ WebSocket connections established
✅ Peer node download working
✅ No race conditions or threading errors
Migration Notes
No changes required for existing code using DefaultNodeManager. The wrapper handles delegation automatically based on SystemInfo.isServer().
Optional: Can directly use ServerNodeManager or ClientNodeManager in Koin modules for clarity:
1
2
3
4
5
6
7
single<NodeManager> {
if (SystemInfo.isServer()) {
ServerNodeManager(get(), get(), get(), installId(), hostName, get())
} else {
ClientNodeManager(get(), get(), installId(), get())
}
}
Future Enhancements
- Client-Side Observation Optimization
- Could track visible nodes in UI
- Only observe nodes currently rendered
- Unobserve when scrolled out of view
- Server Clustering
- Could add consensus protocol
- Distributed actor pattern
- Leader election for certain operations
- Memory Management
- LRU cache for inactive nodes
- Pagination for large node sets
- Automatic cleanup of old nodes
Complexity Reduction
Before:
- 1 class with 958 lines
- Mixed server/client logic throughout
- Multiple
SystemInfo.isServer()checks - Actor pattern for all platforms (unnecessary on client)
After:
- Base class: 169 lines (common functionality)
- ServerNodeManager: 219 lines (server-specific with actor)
- ClientNodeManager: 134 lines (client-specific, no actor)
- DefaultNodeManager: 13 lines (compatibility wrapper)
- Total: 535 lines (45% reduction)
- Zero
SystemInfo.isServer()checks in implementations
Related Documents
Conclusion
The refactored NodeManager architecture provides:
- Clear separation between server and client concerns
- Optimized implementations for each use case
- Better maintainability and testability
- Improved performance through appropriate patterns
- Foundation for future enhancements
The server implementation prioritizes data integrity and cluster management while the client implementation prioritizes UI reactivity and user experience.