@startuml Event Bus Architecture skinparam componentAlign center title Event Bus: In-Process Pub/Sub Architecture package "Publishers" { [Workflow Goroutine 1\n(album A, LOSSLESS)] as WF1 [Workflow Goroutine 2\n(album B, LOSSY)] as WF2 } database "PostgreSQL" as DB { [workflow_runs] as WR [album_events] as AE } package "Event Bus (in-memory)" { [Topic: albumA:LOSSLESS] as T1 [Topic: albumB:LOSSY] as T2 [Global Subscribers] as GS } package "Subscribers" { [MonitorAlbumStream\nClient A (album A)] as S1 [MonitorAlbumStream\nClient B (album A)] as S2 [SubscribeEvents\nClient C (global)] as S3 } WF1 --> DB : 1. Write event\n(synchronous) WF1 --> T1 : 2. Publish\n(async notification) WF2 --> DB : 1. Write event WF2 --> T2 : 2. Publish T1 --> S1 : Ring buffer\n(per subscriber) T1 --> S2 : Ring buffer T1 --> GS T2 --> GS GS --> S3 : Ring buffer note right of DB **Source of truth.** Events survive restarts. Replay via seq numbers. end note note right of T1 **Ephemeral notification.** Ring buffer per subscriber. Slow subscribers: overwrite oldest. No backpressure on publishers. end note note bottom of S1 Client disconnect removes subscriber from topic. Workflow continues. end note == Subscription Lifecycle == note as N1 **Subscribe flow:** 1. Client calls MonitorAlbumStream or SubscribeEvents 2. Server subscribes to EventBus (per-topic or global) 3. Server queries DB for historical events (replay) 4. Server bridges: EventBus → gRPC stream 5. On disconnect: cleanup func unsubscribes **Topic cleanup:** When last subscriber leaves AND workflow completed: topic removed from EventBus map. end note == Recovery on Restart == note as N2 **Server restart recovery:** 1. Query workflow_runs WHERE status = 'running' 2. For each stale run: - If active download exists → mark completed - Otherwise → mark failed ("server restarted") 3. RecoverOrphanedDownloads reschedules poll jobs 4. New workflows start fresh (no goroutine resurrection) end note @enduml