Skip to content

Service DI: Infra bundle, FromRef-composed services, one-axis module tree, worker supervision - #228

Merged
martsokha merged 4 commits into
mainfrom
refactor/di-compose-services
Aug 12, 2026
Merged

Service DI: Infra bundle, FromRef-composed services, one-axis module tree, worker supervision#228
martsokha merged 4 commits into
mainfrom
refactor/di-compose-services

Conversation

@martsokha

Copy link
Copy Markdown
Member

Summary

Pure restructuring of the nvisy-server service layer (no behavior change). Answers three questions raised in review: is the DI nesting idiomatic, how should service/ be organized, and how to kill the repetitive worker setup in main.rs.

1. Dependency injection — Infra + FromRef-composed services

  • Bundle the three ambient clients (Postgres, NATS, crypto) into one cheap-clone Infra handle so services/workers stop re-listing the trio.
  • Stop storing stateless services in ServiceState — compose them from Infra in their FromRef impls (a pure Arc-move, free per request). Only the two stateful singletons (ConnectionSyncService's cancellation registry, HealthCache's snapshot) remain stored.
  • ServiceState drops from 15 fields → 8; workers reach through Infra/their service instead of duplicating deps (DetectionWorker 8→6 constructor args; sync/webhook/retention likewise).
  • DI wiring is three small macros: impl_di_infra! (clone one client out of Infra), impl_di_field! (clone a stored field), impl_di_compose! (build a stateless service from Infra). The detection worker composes its collaborators through the same FromRef wiring handlers use — one graph, not two.

2. Naming

Before After Why
RetentionWorker FileRetentionWorker it expires files specifically
DetectionService DetectionQueue enqueue + status broadcast handle
BlobService RunBlobStore pipeline-run document/redacted/audit I/O
ObjectService ExternalObjectStore bridges external tenant stores
WebhookWorker WebhookDeliveryWorker delivers payloads to endpoints

Plus file renames to match (connection.rsconnection_config.rs, etc.).

3. service/ structure — one organizing axis

Folder ⇔ multi-concept subsystem; flat file ⇔ exactly one type.

  • Flattened notification/notification.rs and retention/file_retention.rs (folders wrapping one type behind a re-export mod.rs that earned nothing).
  • Dissolved security/ (a junk drawer): session_keys/user_agent become root files; password handling — the one member with an internal split — becomes password/ (service + hasher + strength).
  • Genuine subsystems keep their folders (crypto, engine, health, detection, sync, webhook).

4. Worker supervision + module visibility

  • New Worker trait (name + run-until-cancelled, associated Output so each worker keeps its own Result) and a WorkerSet supervisor own the spawn/cancel/join-and-log lifecycle once. ServiceState::spawn_workers replaces the four builders; main.rs::run drops from ~80 lines to ~10.
  • Every service/ submodule is now private mod with the public API surfaced through the mod.rs facade (matching how the crate is consumed everywhere — crate::service::X, never the submodule path). crypto/engine were needlessly pub mod.

Testing

Full gate green at each of the four commits: check / fmt / clippy -D warnings / doc -D warnings / test.

🤖 Generated with Claude Code

martsokha and others added 3 commits August 12, 2026 15:29
Group the three ambient clients (Postgres, NATS, crypto) into a single
`Infra` handle so services and workers stop re-listing the trio, and stop
storing stateless services in `ServiceState` — compose them from `Infra`
in their `FromRef` impls (a pure Arc-move, free per request). Only the two
stateful singletons (`ConnectionSyncService`'s cancellation registry,
`HealthCache`'s snapshot) remain stored. `ServiceState` drops 15 fields to 8;
workers reach through `Infra`/their service rather than duplicating deps
(`DetectionWorker` 8->6 args, sync/webhook/retention workers likewise).

DI wiring is three small macros: `impl_di_infra!` (clone one client out of
`Infra`), `impl_di_field!` (clone a stored field), `impl_di_compose!` (build
a stateless service from `Infra`). The detection worker composes its
collaborators through the same `FromRef` wiring handlers use, so there is one
graph, not two.

Renames for clarity:
- RetentionWorker  -> FileRetentionWorker
- DetectionService -> DetectionQueue          (enqueue + status broadcast)
- BlobService      -> RunBlobStore            (run document/redacted/audit I/O)
- ObjectService    -> ExternalObjectStore     (external tenant stores)
- WebhookWorker    -> WebhookDeliveryWorker
- service/connection.rs -> connection_config.rs (holds only ConnectionConfig)
- service/blob.rs -> run_blob_store.rs, service/object.rs -> external_object_store.rs

Full gate green (check / fmt / clippy / doc / test).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… file

Make the module tree express a single rule — a folder holds a multi-concept
subsystem, a flat file holds exactly one type:

- Flatten `notification/` -> `notification.rs` and `retention/` ->
  `file_retention.rs`; both were folders wrapping one type behind a re-export
  `mod.rs` that earned nothing.
- Dissolve `security/` (a junk drawer of unrelated concerns): `session_keys`
  and `user_agent` become root files (peers of `avatar`/`infra`), and password
  handling — the one member with its own internal split — becomes `password/`
  (`service.rs` + `hasher.rs` + `strength.rs`).

Genuine subsystems keep their folders unchanged (`crypto/`, `engine/`,
`health/`, `detection/`, `sync/`, `webhook/`). The public facade in
`service/mod.rs` is the only external contract, so no call sites outside
`service/` change.

Full gate green (check / fmt / clippy / doc / test).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sibility

Collapse the repeated spawn/cancel/join-and-log boilerplate in main.rs. A new
`Worker` trait (name + run-until-cancelled, with an associated `Output` so each
worker keeps its own `Result` alias) and a `WorkerSet` supervisor own the
lifecycle once: `spawn` under one shared cancellation token, `shutdown` cancels
and joins them all, logging (never propagating) a panicked task. All four
workers implement it, and `ServiceState::spawn_workers` replaces the four
per-worker builder methods. `run()` in main drops from ~80 lines to ~10.

Also normalize service/ module visibility: every submodule is now private
`mod`, with the public API surfaced through the `mod.rs` facade (matching how
the crate is already consumed everywhere — via `crate::service::X`, never the
submodule path). `crypto`/`engine` were needlessly `pub mod` while nothing used
those paths; `CryptoError` is now surfaced via a `pub(crate) use` in the facade
instead of a `service::crypto::` reach-through.

Full gate green (check / fmt / clippy / doc / test).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@martsokha martsokha added cli server entry point, configuration server API handlers, middleware, auth refactor code restructuring without behavior change labels Aug 12, 2026
@martsokha martsokha self-assigned this Aug 12, 2026
The DI worker-supervision change moved cancellation into nvisy-server's
`WorkerSet`, so nvisy-cli no longer uses `tokio-util` — cargo-machete flagged
it. Remove the dependency.

Also remove the `machete` job from build.yml: it duplicated the "Unused
Dependencies" job in security.yml (both just run `cargo machete`). Keep the
security.yml one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@martsokha
martsokha merged commit f60aa45 into main Aug 12, 2026
8 checks passed
@martsokha
martsokha deleted the refactor/di-compose-services branch August 12, 2026 14:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli server entry point, configuration refactor code restructuring without behavior change server API handlers, middleware, auth

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant