Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

13 - State Management (Health Module)

Per-screen Cubit/Bloc design on top of 00-shared/06 conventions (stack: flutter_bloc + get_it; server state via dio repository; LoadState = Initial/Loading/Success/Error(ApiException)). Screens (proposed). Backend truth: single stateless endpoint (health.controller.ts:27-37) - all freshness logic lives client-side.


1. Cubit map

CubitScreen (05)Data
HealthCubit1 (App Health)HealthSnapshot (per-dependency status + extras + freshness), poll timer, retry
(none for S2)2 (Incident sheet)renders from HealthCubit cached payload - no own cubit

Repository (HealthRepository in features/health/data/) is the only layer touching HTTP; it maps both the 200 envelope and the 503 error envelope to models and throws ApiException(status, message) (00-shared/06 §2-3).

2. HealthCubit (core)

stateDiagram-v2
    [*] --> Initial
    Initial --> Loading: fetch()
    Loading --> AllUp(snapshot): 200 envelope, status ok
    Loading --> PartialDown(snapshot): 503 error, <5 down
    Loading --> AllDown(snapshot): 503 error, 5 down
    Loading --> Error(offline): network/timeout (LoadState.Error)
    AllUp/PartialDown/AllDown --> Polling: timer starts (30 s)
    Polling --> AllUp/PartialDown/AllDown: silent refetch (timer reset)
    Polling --> RateLimited: 429 → pause timer, countdown
    RateLimited --> Polling: 60 s elapsed → refetch
    Polling --> Error: network error (keep last snapshot + stale flag)
    Error --> Polling: retry()/connectivity regained
  • Polling: Timer.periodic(30 s) while foregrounded; canceled on background (AppLifecycleState.paused) and restarted with an immediate fetch() on resume (10 I7). 30 s chosen to stay far under the 30 req/min public cap shared with LBs (12 §5, rate-limit.constants.ts:4).
  • Retry: manual retry() = single immediate fetch; on success resets the poll timer (skip next tick). In-flight guard: one request at a time (08 §1).

3. Models

class HealthSnapshot {
  final DateTime checkedAt;            // client clock (server sends none - 12 §6)
  final Map<String, DependencyStatus> deps; // mongodb, redis, storage, bullmq
  final bool reachable;                // HTTP 200 received
  final bool rateLimited;              // 429 seen
  final List<int>? pendingJobs;        // bullmq only: [emails, in-app, webhook-deliver, dlq]
  final String? ping;                  // redis only: 'PONG'
}

enum DependencyStatus { up, down, stale }   // stale = last data older than 60 s
  • Source mapping: data.info.<key>.statusup; data.error.<key>down (keys present in info stay up on partial failure, health.controller.spec.ts:29-35); extras from data.info.redis.ping (redis-health.indicator.ts:16) and data.info.bullmq.pendingJobs (bullmq-health.indicator.ts:30).
  • API card is reachable (request reached server) - no explicit key.

4. Error envelope handling (503 is data, not failure)

HealthRepository treats 503 as a successful fetch of a down state, not an exception: it maps the error envelope (http-exception.filter.ts:73-81) into a HealthSnapshot with all keys down + reachable: false. Only network errors/timeouts/5xx-without-health-shape become LoadState.Error. This is the single most important client decision in the module (09 §2, 14 QA-1).

5. Cross-cutting

  • No cache layer: the screen is its own freshest state; no persistence - restarting the app refetches. Stateless endpoint = nothing to warm.
  • No domain events: the client does not consume any event stream for health (event-queue-map has no health events); polling is the only source.
  • Rate budget: poll (30 s) + manual retry must respect the shared 30 req/min public cap; on 429 the cubit pauses polling 60 s (12 §5).
  • Permission gating: screen gating is (proposed) - no health.* permission exists (permissions.constants.ts:1-97); when added, gate HealthCubit start on it.
  • Planned: liveness/readiness split polling per endpoint when /live + /ready land (planned) - docs/user-flows/END_TO_END_USER_FLOWS.md:738-740.