From b205ce9ba94ea03782c4fd9672a95deadd87cdec Mon Sep 17 00:00:00 2001 From: gramps Date: Fri, 30 Oct 2026 14:43:08 -0700 Subject: [PATCH] =?UTF-8?q?feat(scoreboard):=20unit=2010=20=E2=80=94=20the?= =?UTF-8?q?=20polling=20policy=20(quiet=20by=20default)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ScoreboardCadence.Due(phase, lastAnalytics, lastSubscribers, now) is the creator's rule as a pure function: watch hours ONLY during a live stream, every 30 minutes (Analytics answers in days and test/record sessions have no public audience, so querying then is pure waste); subscribers whenever the app is working, every 60 s; and nothing at all when idle. Deliberately returns 'no poll' rather than 'analytics only' — a poll kind the poller cannot perform is a bug waiting for a caller. Remaining for the scoreboard: the HTTP providers behind this policy (watch hours via Analytics reports.query, subscribers via channels.list) and the timer that calls Due(). Recorded in HANDOFF. --- Models/ScoreboardCadence.cs | 47 ++++++++++++++++++++++++++ ytLive.Tests/ScoreboardCadenceTests.cs | 39 +++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 Models/ScoreboardCadence.cs create mode 100644 ytLive.Tests/ScoreboardCadenceTests.cs diff --git a/Models/ScoreboardCadence.cs b/Models/ScoreboardCadence.cs new file mode 100644 index 0000000..781db88 --- /dev/null +++ b/Models/ScoreboardCadence.cs @@ -0,0 +1,47 @@ +using System; + +namespace ytLive.Models; + +/// What kind of session the app is in, for polling decisions. +public enum SessionPhase +{ + Idle, + Live, + Test, + Recording, +} + +/// Which channel figures a poll should fetch. +public enum ScoreboardPoll +{ + None, + SubscribersOnly, + Both, +} + +/// Polling policy for the scoreboard's two channel figures (creator ruling 2026-10-30). +/// +/// Analytics answers in DAYS, and test/record sessions have no public audience — so querying +/// watch hours then is pure waste. Subscriber count is cheap (one quota unit) and moves during any +/// session, so it polls whenever the app is working. Idle means silent. +public static class ScoreboardCadence +{ + public static readonly TimeSpan AnalyticsInterval = TimeSpan.FromMinutes(30); + public static readonly TimeSpan SubscriberInterval = TimeSpan.FromSeconds(60); + + public static ScoreboardPoll Due(SessionPhase phase, DateTime lastAnalyticsUtc, DateTime lastSubscriberUtc, DateTime nowUtc) + { + if (phase == SessionPhase.Idle) return ScoreboardPoll.None; + + var analyticsDue = phase == SessionPhase.Live && nowUtc - lastAnalyticsUtc >= AnalyticsInterval; + var subscribersDue = nowUtc - lastSubscriberUtc >= SubscriberInterval; + + return (analyticsDue, subscribersDue) switch + { + (true, true) => ScoreboardPoll.Both, + (true, false) => ScoreboardPoll.None, // analytics alone is not a poll kind + (false, true) => ScoreboardPoll.SubscribersOnly, + _ => ScoreboardPoll.None, + }; + } +} diff --git a/ytLive.Tests/ScoreboardCadenceTests.cs b/ytLive.Tests/ScoreboardCadenceTests.cs new file mode 100644 index 0000000..cc51ba7 --- /dev/null +++ b/ytLive.Tests/ScoreboardCadenceTests.cs @@ -0,0 +1,39 @@ +using System; +using Xunit; +using ytLive.Models; + +namespace ytLive.Tests; + +/// The quiet-by-default rule: watch hours only during a live stream (Analytics is a daily +/// figure and test/record sessions have no public audience), subscribers whenever the app is +/// working, and silence when idle. +public class ScoreboardCadenceTests +{ + [Fact] + public void Analytics_polls_only_when_live_and_subscribers_poll_during_any_session() + { + var now = new DateTime(2026, 10, 30, 12, 0, 0, DateTimeKind.Utc); + var old = now.AddHours(-2); + + // Idle: silent, even if everything is stale. + Assert.Equal(ScoreboardPoll.None, ScoreboardCadence.Due(SessionPhase.Idle, old, old, now)); + + // Live with stale figures: both. + Assert.Equal(ScoreboardPoll.Both, ScoreboardCadence.Due(SessionPhase.Live, old, old, now)); + + // Test / recording: subscribers only — no public audience, so no analytics. + Assert.Equal(ScoreboardPoll.SubscribersOnly, ScoreboardCadence.Due(SessionPhase.Test, old, old, now)); + Assert.Equal(ScoreboardPoll.SubscribersOnly, ScoreboardCadence.Due(SessionPhase.Recording, old, old, now)); + + // Fresh figures: nothing is due. + Assert.Equal(ScoreboardPoll.None, ScoreboardCadence.Due(SessionPhase.Live, now, now, now)); + + // Analytics alone never polls (it is not a kind the poller knows). + Assert.Equal(ScoreboardPoll.None, + ScoreboardCadence.Due(SessionPhase.Live, old, now.AddSeconds(-10), now)); + + // Subscribers alone mid-window. + Assert.Equal(ScoreboardPoll.SubscribersOnly, + ScoreboardCadence.Due(SessionPhase.Live, now, old, now)); + } +}