3 unstable releases
| new 0.2.0 | Feb 1, 2026 |
|---|---|
| 0.1.16 | Dec 21, 2025 |
| 0.1.15 | Dec 21, 2025 |
#413 in Development tools
Used in 2 crates
345KB
7.5K
SLoC
kanban-persistence
Persistence layer for the kanban project management tool. Handles JSON storage, format versioning, and data migration.
Installation
Add to your Cargo.toml:
[dependencies]
kanban-persistence = { path = "../kanban-persistence" }
Features
Progressive Auto-Save
- Dirty Flag Tracking: Changes are marked and queued for persistence
- Debounced Saving: 500ms minimum interval between disk writes to prevent excessive I/O
- Atomic Writes: Temporary file writes with atomic rename for crash safety
- Command Audit Log: All commands are tracked for audit trails
Format Versioning
- V2 JSON Format: Structured format with metadata and version tracking
- Automatic V1→V2 Migration: Legacy files are transparently upgraded on first load
- Backup Creation: V1 files backed up as
.v1.backupbefore migration - Version Detection: Automatic format detection without user intervention
Multi-Instance Support
- Instance IDs: Each application instance has a unique ID for coordination
- Last-Write-Wins: Concurrent modifications resolved by latest timestamp
- File Watching: Detects external changes for reload prompts
- Conflict Resolution: Automatic merging strategies for safe concurrent access
API Reference
JsonFileStore
Main persistence store implementation:
use kanban_persistence::{JsonFileStore, PersistenceStore};
// Create store
let store = JsonFileStore::new("board.json");
// Get instance ID
let instance_id = store.instance_id();
// Save data
let snapshot = StoreSnapshot {
data: serde_json::to_vec(&data)?,
metadata: PersistenceMetadata::new(instance_id),
};
store.save(snapshot).await?;
// Load data (automatically migrates V1 to V2)
let (snapshot, metadata) = store.load().await?;
StateManager (kanban-tui)
Manages state mutations and persistence:
use kanban_tui::state::StateManager;
use kanban_domain::commands::Command;
let mut manager = StateManager::new(Some("board.json".into()));
// Execute command (sets dirty flag)
manager.execute_with_context(
&mut boards,
&mut columns,
&mut cards,
&mut sprints,
&mut archived_cards,
Box::new(CreateCard { /* ... */ }),
)?;
// Periodically save (respects 500ms debounce)
manager.save_if_needed(&snapshot).await?;
// Force save immediately (bypasses debounce)
manager.save_now(&snapshot).await?;
Architecture
kanban-core
↑
└── kanban-domain
↑
└── kanban-persistence
↑
└── kanban-tui (StateManager uses persistence)
Command Pattern Flow
- Event Handler collects data and creates Command
- Command is executed via StateManager::execute_command()
- CommandContext applies mutation to data
- Dirty Flag is set by StateManager
- Periodic Timer calls save_if_needed()
- Debounce Check ensures 500ms minimum interval
- Atomic Write saves to disk with temp file + rename
Data Flow
User Input
↓
Event Handler
↓
Command Creation
↓
StateManager::execute_command()
↓
CommandContext::execute()
↓
Data Mutation
↓
Dirty Flag = true
↓
[500ms timer]
↓
StateManager::save_if_needed()
↓
JsonFileStore::save()
↓
Atomic Write
↓
Disk (persisted)
Format Specification
V2 Format
{
"version": 2,
"metadata": {
"instance_id": "uuid-here",
"saved_at": "2024-01-15T10:30:00Z"
},
"data": {
"boards": [],
"columns": [],
"cards": [],
"sprints": [],
"archived_cards": []
}
}
V1 Format (Deprecated)
Legacy format without version field or metadata:
{
"boards": [],
"columns": [],
"cards": [],
"sprints": []
}
Migration automatically adds metadata and wraps data.
Migration Strategy
Automatic V1→V2 Migration
- Detection:
Migrator::detect_version()checks forversionfield - Backup: Original V1 file copied to
.v1.backup - Transform: Data wrapped with V2 metadata
- Write: Migrated file written atomically
- Logging: Migration progress logged for user visibility
Manual Migration
use kanban_persistence::migration::{Migrator, FormatVersion};
// Detect current version
let version = Migrator::detect_version("board.json").await?;
// Migrate if needed
if version == FormatVersion::V1 {
Migrator::migrate(FormatVersion::V1, FormatVersion::V2, "board.json").await?;
}
Performance Characteristics
Debouncing Benefits
- Reduced I/O: Prevents disk thrashing during rapid edits
- Better Responsiveness: 500ms debounce balances persistence with UI responsiveness
- Predictable Load: Steady-state save frequency ~2 saves/second maximum
Atomic Write Safety
- Crash Safety: Incomplete writes cannot corrupt file
- Two-Phase Commit: Write to temp, then atomic rename
- Recovery: Interrupted writes leave original file intact
Examples
Setting up Progressive Save
use kanban_tui::state::StateManager;
use tokio::time::{interval, Duration};
let mut manager = StateManager::new(Some("board.json".into()));
// Periodic save task (runs in background)
tokio::spawn(async move {
let mut save_interval = interval(Duration::from_millis(100));
loop {
save_interval.tick().await;
// Respects 500ms debounce internally
if let Err(e) = manager.save_if_needed(&snapshot).await {
tracing::error!("Failed to save: {}", e);
}
}
});
Handling Concurrent Modifications
// When file is modified externally (multi-instance editing)
// JsonFileStore detects the change via file watching
// Application can prompt user for reload with conflict resolution
// Last-write-wins strategy automatically applied
Error Handling
All public APIs return KanbanResult<T>:
use kanban_persistence::JsonFileStore;
match store.load().await {
Ok((snapshot, metadata)) => {
// Handle loaded data
}
Err(e) => {
// Could be serialization error, missing file, or version error
eprintln!("Failed to load: {}", e);
}
}
Dependencies
kanban-core- Foundation types and traitskanban-domain- Domain modelsserde,serde_json- Serializationtokio- Async runtimeuuid- ID generationchrono- Timestampsasync-trait- Async trait supportthiserror- Error handlingnotify- File watching
License
Apache 2.0 - See LICENSE.md for details
Dependencies
~12–29MB
~330K SLoC