13 - State Management (Webhooks Module)
- 1. Cubit map
- 2. WebhookListCubit
- 3. WebhookDetailCubit (config + metrics)
- 4. Delivery/Retry state machine (the core diagram)
- 5. DeliveryLogListCubit
- 6. WebhookFormCubit
- 7. Cross-cutting
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)). Mermaid diagrams included.
1. Cubit map
| Cubit | Screen(s) (05) | Data |
|---|---|---|
WebhookListCubit | 1 | List<Webhook>, per-row actions |
WebhookFormCubit | 2/4 | form model (name, url, events, secret, enabled), field errors, submit |
WebhookDetailCubit | 3 | Webhook, metrics {total, success, failed, pending} |
DeliveryLogListCubit | 3 preview / 5 | List<DeliveryLog> (≤ 50, webhooks.service.ts:88-93) |
WebhookActionCubit | 7/8/9 | test / retry / pause / resume async actions |
Repositories (WebhookRepository, DeliveryLogRepository in
features/webhooks/data/) are the only layer touching HTTP; they map envelopes
to models and throw ApiException(status, message) (00-shared/06 §2-3).
Models: Webhook (webhook.schema.ts:8-30), DeliveryLog
(webhook-delivery-log.schema.ts:7-39).
2. WebhookListCubit
stateDiagram-v2
[*] --> Initial
Initial --> Loading: fetch()
Loading --> Success: GET /webhooks (200)
Loading --> Error: 401/500
Success --> Loading: pullToRefresh
Success --> Error: refetch fails (keep stale)
Success --> Success: delete OK (optimistic)
Success --> Success: delete 404/500 (rollback + snackbar)
- Fetch:
GET /api/v1/webhooks- no pagination, sortcreatedAt: -1(webhooks.service.ts:35-37). - Delete flow: confirm dialog →
DELETE /api/v1/webhooks/:id(webhooks.controller.ts:45-50) → optimistic row removal; rollback on error.
3. WebhookDetailCubit (config + metrics)
sequenceDiagram
participant S as Screen
participant D as WebhookDetailCubit
participant R as Repo
participant A as API
S->>D: load(id)
D->>R: getById(id) + metrics(id)
R->>A: GET /webhooks/:id | GET /webhooks/:id/metrics
A-->>R: doc | {total, success, failed, pending}
R-->>D: Webhook + Metrics
D-->>S: Success(doc, metrics) / Error(404)
- Independent per-block load: webhook doc and metrics each have their own
LoadState; metrics failure never blanks config (webhooks.service.ts:142-156). - Pause/resume:
WebhookActionCubit→POST /webhooks/:id/pause|resume(webhooks.controller.ts:78-90) → optimisticenabledflip, rollback on error (webhooks.service.ts:158-161).
4. Delivery/Retry state machine (the core diagram)
stateDiagram-v2
[*] --> Idle
Idle --> Enqueueing: test() / retryLatest()
Enqueueing --> Queued: 200 {message: "…queued"}
Enqueueing --> NoFailure: 404 "No failed deliveries to retry" (retry only)
Enqueueing --> Error: 401/500
Queued --> PendingRow: watch logs → newest row status=pending
PendingRow --> SuccessRow: worker 2xx → status=success
PendingRow --> FailedRow: worker non-2xx/timeout → status=failed
FailedRow --> Enqueueing: retryLatest() (manual retry)
FailedRow --> Exhausted: 3 BullMQ attempts used (job rethrow)
Exhausted --> Enqueueing: manual retry re-queues same payload
Queued --> Stale: no row after 20s → "Still queued?" hint
Stale --> PendingRow: row appears
- Test:
POST /webhooks/:id/test(webhooks.controller.ts:65-70) - jobeventType: 'WebhookTested'(webhooks.service.ts:130-138). - Retry:
POST /webhooks/:id/retry(:58-63); server requires a latestfailedlog (webhooks.service.ts:103-108); re-queues that attempt's original payload withcorrelationId: ''(:110-118). - Worker side (server truth): attempts 3 / exponential backoff 5 s
(
webhooks.service.ts:80-83); 10 s fetch timeout (webhook-delivery.worker.ts:81); status transitionspending → success|failed(webhooks.service.ts:163-193,webhook-delivery-log.schema.ts:18-23). - Client implication: a
Queuedstate is terminal for the HTTP action; all delivery transitions are observed via logs polling, not HTTP responses.
5. DeliveryLogListCubit
stateDiagram-v2
[*] --> Initial
Initial --> Loading: fetch(webhookId)
Loading --> Loaded(list): GET /webhooks/:id/logs (≤ 50)
Loading --> Error: 401/500
Loaded --> Loading: pullToRefresh / afterTest / afterRetry
Loaded --> DetailSheet: tap row (no extra fetch)
- Logs endpoint: sort
createdAt: -1, limit 50 (webhooks.service.ts:88-93). - After test/retry the cubit refreshes on a short delay (2-3 s) then again on
user pull; a
pendingnewest row keeps a light "processing" indicator (webhook-delivery-log.schema.ts:22-23).
6. WebhookFormCubit
stateDiagram-v2
[*] --> Idle
Idle --> Validating: submit()
Validating --> Submitting: client valid
Validating --> FieldError: invalid (name/url/events/secret)
Submitting --> Done: POST 201 / PATCH 200 (pop to detail)
Submitting --> ServerError: 400 → field messages; 401/500 → error state
Done --> [*]
- Create:
POST /api/v1/webhooks(webhooks.controller.ts:21-25) body fromCreateWebhookDto(create-webhook.dto.ts:11-34). - Edit:
PATCH /api/v1/webhooks/:idwith dirty fields only (UpdateWebhookDto,update-webhook.dto.ts:4); 404 → error state. - Server 400 messages mapped to fields (mirrors class-validator rules,
08§4).
7. Cross-cutting
- Cache: webhook list cached in memory per tenant; detail/logs read
cache-first then refresh (offline tolerance,
00-shared/10§2). - Events as hints: the client does NOT consume the domain-event stream for webhook UI; server state is the only truth (logs polling).
- Permission gating: cubits expose
canCreate/canRead/canUpdate/canDeletefrom RBAC (permissions.constants.ts:89-92); UI hides FAB/actions/menus accordingly (e.g. no "New webhook" withoutwebhook.create). - Secret handling: secret kept only in memory in DetailCubit; never cached
to disk; forms never pre-fill it (blank = keep,
08§6). - Planned cubits: inbound-receiver list
(planned)perIMPLEMENTATION_PLAN.md:48- no data contract yet.