@startuml seq-polling !theme plain title sofarr — Background Polling Cycle participant "index.js\n(startup)" as entry participant "Poller" as poller participant "Config" as config participant "SABnzbd\n(per instance)" as sab participant "Sonarr\n(per instance)" as sonarr participant "Radarr\n(per instance)" as radarr participant "qBittorrent\nClient" as qbt participant "MemoryCache" as cache == Startup == entry -> poller : startPoller() activate poller alt POLL_INTERVAL > 0 poller -> poller : pollAllServices() (immediate) poller -> poller : setInterval(pollAllServices,\nPOLL_INTERVAL) else POLL_INTERVAL = 0 poller --> entry : "Polling disabled, on-demand mode" end == Poll Cycle == poller -> poller : Check: polling flag?\n(skip if concurrent) poller -> poller : polling = true poller -> poller : start = Date.now() poller -> config : getSABnzbdInstances() config --> poller : [{ id, url, apiKey }] poller -> config : getSonarrInstances() config --> poller : [{ id, url, apiKey }] poller -> config : getRadarrInstances() config --> poller : [{ id, url, apiKey }] note over poller, cache All 9 fetches run in parallel via Promise.all, each wrapped in timed(). Shown sequentially below. end note group Parallel API Fetches (Promise.all) poller -> sab : GET /api?mode=queue sab --> poller : { queue: { slots, status, speed } } poller -> sab : GET /api?mode=history&limit=10 sab --> poller : { history: { slots } } poller -> sonarr : GET /api/v3/tag sonarr --> poller : [{ id, label }] poller -> sonarr : GET /api/v3/queue?includeSeries=true sonarr --> poller : { records: [{ seriesId, series, ... }] } poller -> sonarr : GET /api/v3/history?pageSize=10 sonarr --> poller : { records: [{ seriesId, ... }] } poller -> radarr : GET /api/v3/queue?includeMovie=true radarr --> poller : { records: [{ movieId, movie, ... }] } poller -> radarr : GET /api/v3/history?pageSize=10 radarr --> poller : { records: [{ movieId, ... }] } poller -> radarr : GET /api/v3/tag radarr --> poller : [{ id, label }] poller -> qbt : getTorrents() qbt --> poller : [{ name, progress, ... }] end poller -> poller : Record per-task timings\nlastPollTimings = { totalMs,\ntimestamp, tasks: [{label, ms}] } poller -> poller : cacheTTL = POLL_INTERVAL × 3 poller -> cache : set('poll:sab-queue', ..., cacheTTL) poller -> cache : set('poll:sab-history', ..., cacheTTL) poller -> cache : set('poll:sonarr-tags', ..., cacheTTL) note over poller : Tag queue records with\n_instanceUrl on embedded\nseries/movie objects poller -> cache : set('poll:sonarr-queue', ..., cacheTTL) poller -> cache : set('poll:sonarr-history', ..., cacheTTL) poller -> cache : set('poll:radarr-queue', ..., cacheTTL) poller -> cache : set('poll:radarr-history', ..., cacheTTL) poller -> cache : set('poll:radarr-tags', ..., cacheTTL) poller -> cache : set('poll:qbittorrent', ..., cacheTTL) poller -> poller : Notify SSE subscribers\npollSubscribers.forEach(cb => cb()) note over poller : Each registered SSE client\ncallback rebuilds its payload\nand writes a data: frame poller -> poller : polling = false\nlog elapsed time deactivate poller @enduml