13 — State Management (Scheduler Module)
- 1. State model (JobsBloc)
- 2. Blocs
- 3. Fetch flow (JobsBloc)
- 4. Cron → queue flow (server, for the console's mental model)
- 5. Manual trigger (S5)
- 6. Cache & staleness
- 7. Error mapping (
00-shared/06 §5)
Bloc-based state for the Scheduled Jobs console per 00-shared/06. Server truth: BullMQ owns job state; the API exposes only the registry (E1-E3 implemented, E4-E8
(proposed)). The console is superadmin-only (platform level,scheduler.controller.ts:24,31,38).
1. State model (JobsBloc)
sealed class JobsState { const JobsState(); }
class JobsInitial extends JobsState {}
class JobsLoading extends JobsState {} // first load
class JobsLoaded extends JobsState {
final List<JobRow> jobs; // {queue,name,pattern,tz} (scheduler.service.ts:152-186)
final Map<String, RunSummary> enrich; // (proposed) lastRunAt,lastStatus,nextRunAt — key queue:name
final DateTime fetchedAt;
}
class JobsError extends JobsState {
final AppError error; // 401/403/429/5xx
final List<JobRow>? lastGood; // keep-last-good
}
JobRow mirrors scheduler.service.ts:152-186 exactly — no client-side
derivation (09 B8/B9). Status badges read only enrich when present,
else unknown (07 C1).
2. Blocs
| Bloc | Owns | Source |
|---|---|---|
JobsBloc | registry list, enrich map, poll loop | E1 + E4 (proposed) |
JobDetailBloc | one job's definition + run history | E1 + E4 |
CreateScheduleBloc | F1 form state, submit, duplicate warn | E2 |
DlqBloc | DLQ records, replay/delete | E7/E8 (proposed) |
LogsBloc | S4 log stream, level filter, tail-live | E5 (proposed) |
Single JobsBloc per /admin/scheduler route, provided in the screen,
disposed on pop (poll timer dies with it).
3. Fetch flow (JobsBloc)
flowchart TD
A[open /admin/scheduler] --> B{lastGood?}
B -->|no| C[emit Loading]
C --> D[GET /scheduler]
D -->|200| E[emit Loaded jobs fetchedAt=now]
D -->|401| F[refresh → replay → fail = session expiry]
D -->|403| G[emit permission-empty]
D -->|429| H[emit Error, skip next poll]
D -->|5xx| I[emit Error keep lastGood]
E --> J[Timer 60s]
J --> K{enrich available? E4}
K -->|yes| L[GET /scheduler/runs?bulk]
K -->|no| M[skip — unknown badges]
L --> E
- Single-flight + coalescing (10 §2);
refresh({force})for pull-to-refresh. - Background: timer paused, resume → immediate staleness check (
09B3).
4. Cron → queue flow (server, for the console's mental model)
flowchart LR
subgraph Boot
M[onModuleInit<br/>scheduler.service.ts:43-45] --> R[registerDefaults<br/>10 jobs :47-126]
R -->|queue.add repeat pattern tz UTC| BQ[(BullMQ repeatables<br/>Redis)]
end
BQ -->|fires on schedule| Q[queue.add child job<br/>jobs/*.job.ts]
Q --> W[Worker<br/>e.g. report.worker.ts]
W -->|tenantContext.run| T[process<br/>tenant-scoped]
T -->|fail| RT{attempts 3<br/>exp 5s :60-65}
RT -->|retry| Q
RT -->|exhausted| DLQ[DLQ<br/>attemptsMade>=attempts<br/>dlq.setup.ts:8-9]
T -->|ok| DONE[done]
5. Manual trigger (S5)
TriggerBloc-free: JobDetailBloc.trigger() → confirm dialog (F2) →
E6 (proposed) → on success refetch run history; button disabled while
in-flight (10 §4). If E6 absent → button disabled with tooltip.
6. Cache & staleness
| Layer | Scope | TTL | Writes on |
|---|---|---|---|
| Server | BullMQ state | retention 1 h / 100 (triggers), 14 d failed (bullmq.module.ts:63-64) | every run |
| Client volatile | registry list sl:scheduler:jobs | 60 s | successful E1 only |
| Client render | — | — | last-good kept; stale banner past 60 s |
7. Error mapping (00-shared/06 §5)
| Error | UI |
|---|---|
| 401 | silent refresh → replay; fail → session expiry |
| 403 | console hidden (superadmin gate, scheduler.controller.ts:24) |
| 429 | skip next poll; snackbar once |
| 404 | job removed concurrently → drop row |
| 5xx | keep lastGood; snackbar ≥ 4 s; no skeleton flash |