13 — State Management (Reports Module)
- 1. Cubits
- 2. Job state machine (server-authoritative)
- 3. Data persistence
- 4. Loading / streaming / realtime
Cubit/BloC layout for the Reports module per 00-shared/06 (recommendation, not an implemented decision). One Cubit per screen cluster; polling owned by a single engine. Includes the job state machine.
1. Cubits
ReportCatalogCubit (S1)
- State:
CatalogState { templates: ReportTemplate[] }— static; loads once;(forward-looking)fetches server-supported types. - No async beyond init.
GenerateReportCubit (S2)
- State:
GenerateState { type, params, submitting, fieldErrors }. - Events:
SelectType,UpdateParam,Submit. Submit: validate (client presence rule 08 §3.1) →POST /reports/generate→ success: emitjobCreated(jobId)→ router to S4 (reports.controller.ts:14-18).- Server 400 maps to
fieldErrors; 429/5xx to snackbar (12 §client table).
ReportJobListCubit (S3)
- State:
JobListState { jobs: LocalJobEntry[], filter, loading }. LocalJobEntry={jobId, type, status, createdAt, params?, source}from the local history store (12 E4 gap) — refreshed by polling visible rows.- Events:
LoadHistory,Refresh,Filter,Retry(jobId).
ReportJobDetailCubit (S4) — owns the polling engine
- State:
JobDetailState { job?, status, elapsed, pollPaused, error? }. - Events:
Open(jobId),PollTick,Retry,CheckAgain(manual after pause). - Polling engine (10 §1):
Timer.periodic(2s)while status non-terminal; GET → map doc; terminal → cancel timer; network/5xx → pause + emitpollPaused; 404 → terminal not-found. Retry: re-POSTparams(reports.service.ts:31-43) → pushReplacement detail of new job.
ReportResultCubit (S5)
- State:
ResultState { job, renderModel, exporting }. - Pure mapping from
job.resultper type (06 §S5 renderers,reports.service.ts:96-160). Export(planned): no-op/disabled today; future: E3 download → share sheet.
2. Job state machine (server-authoritative)
Statuses from report-job.schema.ts:13-18; transitions applied by
report-job.repository.ts:21-47 (markProcessing/markCompleted/markFailed) and
the initial queued default (report-job.schema.ts:28-29). The Cubit mirrors,
never predicts.
stateDiagram-v2
[*] --> queued: job created (POST /reports/generate)\nreports.service.ts:31-43
queued --> processing: worker executes\n(reports.service.ts:59, markProcessing)
processing --> completed: result stored\n(markCompleted, repo :27-38)
processing --> failed: error stored\n(markFailed, repo :40-47)
queued --> failed: worker error path\n(reports.service.ts:79-82)
failed --> queued: user Retry = NEW job POST\n(new jobId, old stays failed)
completed --> [*]: result readable via GET\n(reports.service.ts:46-50)
failed --> [*]
Notes:
queued → failedwithoutprocessingis possible (executor throws before markProcessing completes —reports.service.ts:59-82); UI must not assume processing was seen.- No
processing → queuedrequeue exists in code — a crashed worker leaves a job stuck inprocessing(QA item,14 §5). - Retry is never a status transition on the same doc — always a new job.
3. Data persistence
| Store | Key | TTL | Purpose |
|---|---|---|---|
| local history | reports.localHistory | cap 50 entries | S3 list (E4 gap) |
| last-good job doc | reports.job.{jobId} | 24 h | S4 offline render |
| last-good result | reports.result.{jobId} | 24 h | S5 offline render |
Server remains source of truth; caches are read-only mirrors.
4. Loading / streaming / realtime
| Screen | Loading | Streaming | Realtime |
|---|---|---|---|
| S1 | none (static) | — | — |
| S2 | submit spinner | — | — |
| S3 | skeleton rows | — | poll visible rows on refresh |
| S4 | full skeleton | — | poll 2 s (the module's realtime) |
| S5 | render on doc | — | — (terminal) |
No WS topic for reports (00-shared/07 §8 has none) — (forward-looking):
push notification on ReportGenerated (04-Modules/Reports.md:33).