13 — State Management (Audit Module)
- 1. AuditCubit — list + filters + pagination (single owner of list state)
- 2. AuditDetailState (in-memory; no refetch — no
:idendpoint, OQ-4) - 3. WS subscription (realtime append)
- 4. Caching & staleness
- 5. Cross-cutting interplay
- 6. Error states per action
- 7. Testing hooks (
00-shared/06 §6)
Per-screen Cubits (Flutter/bloc; proposal, 00-shared/06) for the read-only audit trail. Backed by
AuditRepository(dio) callingGET /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 event | Cubit | Repository call |
|---|---|---|
| open screen | load() | GET /audit-logs?page=1&limit=50 (audit.controller.ts:20-21) |
| Load more | loadMore() | GET /audit-logs?page={page+1}&limit=50 |
| Pull-to-refresh | pullToRefresh() | GET ?page=1 bypass cache |
| Filter change / clear | changeFilter(f) | GET ?page=1&action=&entityType=&actorId= (only non-empty params, audit.controller.ts:26-29) |
| Retry | retry() | re-run last request |
| WS event | onWsEvent(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 setsnotFound. - 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-joinstenant:{tenantId}(ws.gateway.ts:50). - AuditCubit listens to all
eventTypetopics of the domain-event stream — the same events the handler persists (ws-bridge.service.ts:16-22vsaudit.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;RefreshIndicatorbypasses 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-audituses the sameAuditCubitwith a preset filter (actionin RBAC set) — shared widget + state, different entry route (design-docs/rbac/05 S6).
6. Error states per action
| Action | Error | State → |
|---|---|---|
| load/loadMore | 401 | session-expiry flow |
| load/loadMore | 429 | RATE_LIMITED → countdown, keep items |
| loadMore | 5xx | snackbar + keep isLoadingMore=false (retryable) |
| load | 5xx | error state → AppErrorState |
| load | 400 (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.