Skip to main content

max / makenotwork

Decompose AppState Phase 1: capability slice views Add five projection-view slices (Billing, Integrations, Scanning, Sync, Ops) with hand-written FromRef<AppState>. They're views, not physical nested fields: Sync and Scanning draw from fields living in different existing sub-structs (AppStorage/AppCaches/AppLimiters), so they can't be physical containers. Fields keep their homes; the FromRef clones the relevant ones on extraction. synckit_s3 stays in AppStorage; Sync borrows it. No field moves, no handler-body churn. Phase 2 handlers extract State<Billing> etc. instead of State<AppState>. Extend the from_ref_slices guard test to cover all five plus BackgroundTx. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-13 18:42 UTC
Commit: 4bf6179bd7778ad0e38c77dcbe7cf7fbf51cc5f6
Parent: 9740007
1 file changed, +124 insertions, -0 deletions
@@ -184,6 +184,121 @@ pub struct AppLimiters {
184 184 pub git_smart_http_semaphore: Arc<tokio::sync::Semaphore>,
185 185 }
186 186
187 + // ---------------------------------------------------------------------------
188 + // Capability slices (decomposition Phase 1).
189 + //
190 + // Each of these is a logical grouping of `AppState` fields that a handler can
191 + // extract as `State<Slice>` instead of the whole god struct. They are projection
192 + // *views*, not physical containers: the fields keep their existing homes on
193 + // `AppState` (flat, or inside `AppStorage`/`AppCaches`/`AppLimiters`), and a
194 + // hand-written `FromRef<AppState>` clones the relevant ones on extraction. This
195 + // is what lets a slice like `Sync` or `Scanning` draw from fields that live in
196 + // different physical sub-structs. Every field here is `Arc`/pool/small-`Clone`,
197 + // so the projection clone is cheap. Single-field slices (e.g. `State<PgPool>`,
198 + // `State<EmailClient>`, `State<BackgroundTx>`) come free from the `AppState`
199 + // derive and don't need a struct here.
200 + //
201 + // See `_private/docs/mnw/appstate-decomposition-plan.md`.
202 + // ---------------------------------------------------------------------------
203 +
204 + /// Billing/pricing slice: the Stripe provider plus the pricing tables derived
205 + /// from `assumptions.toml`. Used by checkout, subscription, promo, and the
206 + /// public `/pricing` page.
207 + #[derive(Clone)]
208 + pub struct Billing {
209 + pub stripe: Option<Arc<dyn PaymentProvider>>,
210 + pub tier_prices: tier_prices::TierPrices,
211 + pub runway_config: tier_prices::RunwayConfig,
212 + pub pricing_comparison: pricing_comparison::PricingComparison,
213 + }
214 +
215 + impl FromRef<AppState> for Billing {
216 + fn from_ref(s: &AppState) -> Self {
217 + Self {
218 + stripe: s.stripe.clone(),
219 + tier_prices: s.tier_prices.clone(),
220 + runway_config: s.runway_config.clone(),
221 + pricing_comparison: s.pricing_comparison.clone(),
222 + }
223 + }
224 + }
225 +
226 + /// External-service integration clients (both optional, tailnet/internal).
227 + #[derive(Clone)]
228 + pub struct Integrations {
229 + pub mt_client: Option<mt_client::MtClient>,
230 + pub wam: Option<wam_client::WamClient>,
231 + }
232 +
233 + impl FromRef<AppState> for Integrations {
234 + fn from_ref(s: &AppState) -> Self {
235 + Self {
236 + mt_client: s.mt_client.clone(),
237 + wam: s.wam.clone(),
238 + }
239 + }
240 + }
241 +
242 + /// File-scanning slice: the scan pipeline plus the semaphore that bounds
243 + /// concurrent in-memory scans. Draws `scanner` from the top level and
244 + /// `scan_semaphore` from [`AppLimiters`].
245 + #[derive(Clone)]
246 + pub struct Scanning {
247 + pub scanner: Option<Arc<ScanPipeline>>,
248 + pub scan_semaphore: Arc<tokio::sync::Semaphore>,
249 + }
250 +
251 + impl FromRef<AppState> for Scanning {
252 + fn from_ref(s: &AppState) -> Self {
253 + Self {
254 + scanner: s.scanner.clone(),
255 + scan_semaphore: s.limiters.scan_semaphore.clone(),
256 + }
257 + }
258 + }
259 +
260 + /// SyncKit slice: the blob bucket plus the SSE push/rate-limit maps. Draws
261 + /// `synckit_s3` from [`AppStorage`] and the two maps from [`AppCaches`].
262 + #[derive(Clone)]
263 + pub struct Sync {
264 + pub synckit_s3: Option<Arc<dyn StorageBackend>>,
265 + pub sync_notify: Arc<DashMap<(SyncAppId, UserId), tokio::sync::broadcast::Sender<i64>>>,
266 + pub sse_connections: Arc<DashMap<UserId, std::sync::atomic::AtomicUsize>>,
267 + }
268 +
269 + impl FromRef<AppState> for Sync {
270 + fn from_ref(s: &AppState) -> Self {
271 + Self {
272 + synckit_s3: s.storage.synckit_s3.clone(),
273 + sync_notify: s.caches.sync_notify.clone(),
274 + sse_connections: s.caches.sse_connections.clone(),
275 + }
276 + }
277 + }
278 +
279 + /// Observability/lifecycle slice: uptime clocks, the pending-restart flag, and
280 + /// the Prometheus render handle. Used by status/health/admin/metrics handlers.
281 + /// (`started_at`/`start_instant` are `#[from_ref(skip)]` on `AppState`, so this
282 + /// view is the only way to extract them without the whole struct.)
283 + #[derive(Clone)]
284 + pub struct Ops {
285 + pub started_at: chrono::DateTime<chrono::Utc>,
286 + pub start_instant: Instant,
287 + pub restart_at: Arc<std::sync::atomic::AtomicI64>,
288 + pub metrics_handle: Option<metrics_exporter_prometheus::PrometheusHandle>,
289 + }
290 +
291 + impl FromRef<AppState> for Ops {
292 + fn from_ref(s: &AppState) -> Self {
293 + Self {
294 + started_at: s.started_at,
295 + start_instant: s.start_instant,
296 + restart_at: s.restart_at.clone(),
297 + metrics_handle: s.metrics_handle.clone(),
298 + }
299 + }
300 + }
301 +
187 302 /// Externally-wired dependencies handed to [`AppState::build`].
188 303 ///
189 304 /// Holds every field the caller must construct from the environment (pools,
@@ -570,12 +685,21 @@ mod from_ref_slices {
570 685
571 686 #[test]
572 687 fn slices_are_extractable() {
688 + // Single-field slices from the AppState derive.
573 689 _assert_from_ref::<sqlx::PgPool>();
574 690 _assert_from_ref::<Config>();
575 691 _assert_from_ref::<EmailClient>();
692 + _assert_from_ref::<background::BackgroundTx>();
693 + // Physical sub-structs (also from the derive).
576 694 _assert_from_ref::<AppStorage>();
577 695 _assert_from_ref::<AppCaches>();
578 696 _assert_from_ref::<AppLimiters>();
697 + // Phase 1 projection views (hand-written FromRef).
698 + _assert_from_ref::<Billing>();
699 + _assert_from_ref::<Integrations>();
700 + _assert_from_ref::<Scanning>();
701 + _assert_from_ref::<Sync>();
702 + _assert_from_ref::<Ops>();
579 703 }
580 704 }
581 705