ADR-015: Frame Storage Port Abstraction
Status: Accepted
Date: 2026-04-03
Scope: maekon-core ports, maekon-storage adapter, src-tauri composition root
Context
FrameFileStorage is a concrete type in maekon-storage that handles frame image
persistence (WebP files organized by date directories with retention policies).
Currently, 10+ files in src-tauri reference Arc<FrameFileStorage> directly,
bypassing the hexagonal port abstraction that all other storage operations follow.
While this is acceptable per ADR-014 (composition root may reference concrete types
for wiring), the widespread use of FrameFileStorage across scheduler loops, capture
services, automation runtime, and agent support creates tight coupling that hinders:
- Testability — Unit tests cannot mock frame storage without the full filesystem
- Replaceability — Switching to in-memory or cloud storage requires 10+ file changes
- Dependency clarity — Consumers declare dependency on an implementation rather than a capability
Decision
Introduce a FrameStoragePort trait in maekon-core::ports that abstracts the
frame storage operations actually used by consumers:
#[async_trait]
pub trait FrameStoragePort: Send + Sync {
async fn save_frame(&self, timestamp: DateTime<Utc>, data: Vec<u8>)
-> Result<PathBuf, CoreError>;
async fn save_frames_batch(&self, frames: Vec<(DateTime<Utc>, Vec<u8>)>)
-> Result<Vec<PathBuf>, CoreError>;
async fn enforce_retention(&self) -> Result<usize, CoreError>;
async fn enforce_storage_limit(&self) -> Result<usize, CoreError>;
}
FrameFileStorage in maekon-storage implements this trait.
Consumers in src-tauri receive Arc<dyn FrameStoragePort> instead of
Arc<FrameFileStorage>.
Rationale
- ADR-001 §2 alignment: Port traits use
#[async_trait]with&selfreceivers - ADR-001 §3 alignment: DI via
Arc<dyn T>constructor injection - Minimal surface: Only 4 methods that are actually consumed; diagnostic methods
(
frames_dir,buffer_pool_stats,disk_status) remain on the concrete type for composition-root-only access - SOLID compliance: Consumers depend on the capability they need, not the implementation
Consequences
Positive
- Frame storage consumers become testable with mock implementations
- Future storage backends (in-memory, cloud) require zero consumer changes
- Dependency graph is clearer —
maekon-storageis only referenced in wiring code
Negative
- Small runtime overhead from dynamic dispatch (negligible for I/O-bound operations)
- Composition root still needs
Arc<FrameFileStorage>for diagnostic methods
Migration
CaptureContext.frame_storagechanges fromOption<Arc<FrameFileStorage>>toOption<Arc<dyn FrameStoragePort>>- Scheduler, automation runtime, agent support follow the same pattern
- Wiring code (
capture_services.rs,agent_runtime_support.rs) createsArc<FrameFileStorage>and passes it asArc<dyn FrameStoragePort>