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 (Audit Module)

Per-screen Cubits (Flutter/bloc; proposal, 00-shared/06) for the read-only audit trail. Backed by AuditRepository (dio) calling GET /audit-logs (12_API_Mapping.md) and the WS topic stream (ws-bridge.service.ts:16-22).


1. AuditCubit — list + filters + pagination (single owner of list state)

stateDiagram-v2
    [*] --> initial
    initial --> loading : load()
    loading --> loaded(items, total) : page 1 ok
    loading --> error(code) : 4xx/5xx/offline
    loaded --> loading : changeFilter() | pullToRefresh() (page=1)
    loaded --> loadingMore : loadMore() (page+1)
    loadingMore --> loaded : appended items
    loadingMore --> error : failure (keep items, snackbar)
    loaded --> loaded : realtimeAppend(entry) — prepend, dedupe by _id
    error --> loading : retry()

State:

class AuditState {
  final AuditFilter filters;      // action?, entityType?, actorId?
  final List<AuditEntry> items;   // accumulated pages
  final int total;                // envelope.data.total (audit.service.ts:49)
  final int page;                 // last loaded page
  final bool isLoadingMore;
  final LoadState loadState;      // initial|loading|success|error (00-shared/06 §3.1)
  final int? realtimeCount;       // unacknowledged WS appends
}

Events → methods:

UI eventCubitRepository call
open screenload()GET /audit-logs?page=1&limit=50 (audit.controller.ts:20-21)
Load moreloadMore()GET /audit-logs?page={page+1}&limit=50
Pull-to-refreshpullToRefresh()GET ?page=1 bypass cache
Filter change / clearchangeFilter(f)GET ?page=1&action=&entityType=&actorId= (only non-empty params, audit.controller.ts:26-29)
Retryretry()re-run last request
WS eventonWsEvent(event)none — derive AuditEntry from broadcast (eventType, occurredAt, payload); prepend only if matching active filters, dedupe by _id; increment realtimeCount → snackbar/banner

Pagination math: hasMore = items.length < total (server total, no meta.hasNext — OQ-2); page advances by 1 per load; guard: no loadMore while isLoadingMore or during loading.

2. AuditDetailState (in-memory; no refetch — no :id endpoint, OQ-4)

class AuditDetailState {
  final AuditEntry? entry;   // passed from list / deep-link lookup
  final bool notFound;       // deep-link miss after lookup cap
}
  • No loading/network states — the entry object is already in the list payload (12_API_Mapping). If deep link misses, the cubit iterates subsequent pages (cap (proposed)) then sets notFound.
  • Diff computation is a pure function computeDiff(before, after) → (changed, added, removed) (07 §C) — unit-testable, no state.

3. WS subscription (realtime append)

  • Client connects via WsClient (00-shared/11 §5), bearer token in handshake (ws.gateway.ts:37-40); server auto-joins tenant:{tenantId} (ws.gateway.ts:50).
  • AuditCubit listens to all eventType topics of the domain-event stream — the same events the handler persists (ws-bridge.service.ts:16-22 vs audit.handler.ts:18-25), guaranteeing append parity.
  • On WS reconnect: pullToRefresh() (merge + dedupe).
  • Safety: broadcast payloads are unmasked (OQ-8) — the cubit never stores raw broadcast payload fields into a displayed entry; on tap, the row expands from the cached (masked) REST object when available, else shows generic summary.

4. Caching & staleness

  • List: last-good cache key sl:cache:{tenant}:audit:{filters-sha}; TTL 5 min (volatile list, 00-shared/06 §3.3); stale-while-revalidate; RefreshIndicator bypasses cache.
  • Detail: no cache (in-memory only).
  • Filter dropdown option lists (distinct actions/entityTypes): derived from accumulated pages + cached; refreshed on page 1 loads.

5. Cross-cutting interplay

  • AuthCubit: 401 → single-flight refresh → fail → sessionExpired (list reopens after re-login).
  • ConnectivityCubit: offline → AppOfflineBanner + cached list; WS buffered; on reconnect → resubscribe + refetch.
  • FeatureFlagsCubit: none (audit list is not feature-gated).
  • RBAC module: /settings/access-audit uses the same AuditCubit with a preset filter (action in RBAC set) — shared widget + state, different entry route (design-docs/rbac/05 S6).

6. Error states per action

ActionErrorState →
load/loadMore401session-expiry flow
load/loadMore429RATE_LIMITED → countdown, keep items
loadMore5xxsnackbar + keep isLoadingMore=false (retryable)
load5xxerror state → AppErrorState
load400 (bad page/limit)reset page=1

7. Testing hooks (00-shared/06 §6)

  • Unit: pagination math (hasMore/total edge), filter→query mapping, diff computation, WS append dedupe + filter-matching, realtime-count acknowledgement.
  • Widget: loading/error/empty/loaded states; banner; table vs card layout switch.