Problem
Several architecture issues were identified during the v2.2.0 implementation:
1. EventBus lives in api layer, should be in infra
src/api/events.rs contains the EventBus CDC publisher — a general-purpose pub/sub mechanism. It imports from crate::infra::cdc but is itself in the api layer. This violates layering: the infra layer should own the event bus abstraction, and api should only wire it up.
Impact: Any non-API code that wants to subscribe to engine events (e.g., TUI, CLI, Notes) would need to depend on the API module.
2. CDC publisher chain is ad-hoc
The MultiPublisher in src/infra/cdc.rs solves the immediate need to fan out to EventBus + webhook, but it has no backpressure, no error isolation, and no monitoring. A single slow publisher blocks the entire chain synchronously.
3. TransactionManager uses std::sync::Mutex under actix-web
The TransactionManager in src/api/mod.rs protects active transactions with std::sync::Mutex<HashMap<u64, Transaction>>. The take/insert pattern for every txn_put/txn_delete is:
- High contention under load (serializes all transaction mutations)
- Race-prone (forgotten re-inserts leave dangling transactions)
- Not integrated with actix-webs timeout/cleanup — transactions can leak
4. API module is 1900+ lines
src/api/mod.rs has grown to ~1920 lines containing:
- Route registrations
- Handler functions
- Request/response types (Deserialize structs)
- Server startup logic (TLS, CDC, middleware)
- Transaction management
This violates single-responsibility and makes testing hard.
5. No HTTP integration test infrastructure
Unlike the engine (which has extensive unit tests via #[cfg(test)]), there is zero test infrastructure for HTTP-level tests. No shared test helpers, no test server builder, no fixture system.
Proposed Solution
Phase 1: Move EventBus to infra
- Move
EventBus from src/api/events.rs to src/infra/events.rs
- Implement
CdcPublisher for EventBus there
- Add
subscribe() method returning tokio::sync::broadcast::Receiver<CdcEvent>
- Re-export from
src/infra/mod.rs
- Update API module to import from
crate::infra::events
Phase 2: Create API test harness
- Create
src/api/test_helpers.rs with:
fn test_server(engine: LsmEngine) -> impl Service — builds an actix-web test app
fn test_engine() -> (LsmEngine, TempDir) — creates engine with test config
- Common fixture setup functions
- This enables writing integration tests as unit tests in api module or integration tests in tests/
Phase 3: Split api/mod.rs into modules
src/api/routes.rs — all route registrations and handler functions
src/api/types.rs — all request/response Deserialize structs
src/api/server.rs — server startup, TLS, middleware wiring
src/api/transactions.rs — TransactionManager and txn handlers
src/api/mod.rs — re-exports and module declarations
Phase 4: Improve TransactionManager
- Use
tokio::sync::RwLock<HashMap<u64, Transaction>> instead of std::sync::Mutex
- Add TTL-based cleanup for abandoned transactions
- Simplify API to avoid take/insert pattern
Acceptance criteria
Problem
Several architecture issues were identified during the v2.2.0 implementation:
1. EventBus lives in api layer, should be in infra
src/api/events.rscontains theEventBusCDC publisher — a general-purpose pub/sub mechanism. It imports fromcrate::infra::cdcbut is itself in theapilayer. This violates layering: theinfralayer should own the event bus abstraction, andapishould only wire it up.Impact: Any non-API code that wants to subscribe to engine events (e.g., TUI, CLI, Notes) would need to depend on the API module.
2. CDC publisher chain is ad-hoc
The
MultiPublisherinsrc/infra/cdc.rssolves the immediate need to fan out to EventBus + webhook, but it has no backpressure, no error isolation, and no monitoring. A single slow publisher blocks the entire chain synchronously.3. TransactionManager uses std::sync::Mutex under actix-web
The
TransactionManagerinsrc/api/mod.rsprotects active transactions withstd::sync::Mutex<HashMap<u64, Transaction>>. The take/insert pattern for every txn_put/txn_delete is:4. API module is 1900+ lines
src/api/mod.rshas grown to ~1920 lines containing:This violates single-responsibility and makes testing hard.
5. No HTTP integration test infrastructure
Unlike the engine (which has extensive unit tests via
#[cfg(test)]), there is zero test infrastructure for HTTP-level tests. No shared test helpers, no test server builder, no fixture system.Proposed Solution
Phase 1: Move EventBus to infra
EventBusfromsrc/api/events.rstosrc/infra/events.rsCdcPublisherfor EventBus theresubscribe()method returningtokio::sync::broadcast::Receiver<CdcEvent>src/infra/mod.rscrate::infra::eventsPhase 2: Create API test harness
src/api/test_helpers.rswith:fn test_server(engine: LsmEngine) -> impl Service— builds an actix-web test appfn test_engine() -> (LsmEngine, TempDir)— creates engine with test configPhase 3: Split api/mod.rs into modules
src/api/routes.rs— all route registrations and handler functionssrc/api/types.rs— all request/response Deserialize structssrc/api/server.rs— server startup, TLS, middleware wiringsrc/api/transactions.rs— TransactionManager and txn handlerssrc/api/mod.rs— re-exports and module declarationsPhase 4: Improve TransactionManager
tokio::sync::RwLock<HashMap<u64, Transaction>>instead ofstd::sync::MutexAcceptance criteria
crate::infra::events