Tournament-scoped SSE nudges - #307
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements #293 via the issue's preferred design: nudges stay one global stream on the existing single connection, but each nudge's payload now says which tournaments it concerns, so a client watching one tournament can skip refetches another tournament's poll cycle caused. Fully backward compatible —
null(or ignoring the field) means "refetch", exactly the pre-#293 behavior.Plumbing (worker → table → poll loop → SSE frame):
nudge_versions.tournament_ids— nullable JSON column (expand-only migration62ef856f3e47, validated on a throwaway Postgres 16; NULL = unscoped, so mixed-revision rollover is safe).emit_nudge(..., tournament_ids=…)stores a deduped, sorted scope inside the same transaction as the data write;poll_for_nudgesrepublishes it; the SSE frame payload becomes{"polled_at": …, "tournament_ids": [...] | null}.get_tournament_ids_for_profilesroster helper resolves a profile set to the tournaments rostering them.Per-tick scoping (emit cadence unchanged — no freshness regression):
player_stats→ tournaments whose rosters contain the polled batch (the batch is the cross-tournament union, so this is every tournament with a linked tracked entrant — tournaments with empty or unlinked rosters stop waking clients).recent_matches→ tournaments of the profiles whose fetch succeeded this cycle (a failed profile contributed no write).live_matches→ tournaments of the previous live set ∪ the new one, with the previous set read before the wholesale rewrite — so a tournament whose entrant just left a game still gets itsin_matchflip.README's third-party-frontend section documents the payload contract for consumers.
Tests
585 passed. New: roster-helper mapping (shared profile across tournaments, unknown/empty inputs); scoped + unscoped SSE frame rendering; emit→poll→hub round-trip carrying a deduped sorted scope and
None(integration, real Postgres); a two-tournament tick test proving the standings nudge carries only the rostered tournament's id. Existing event/stream/tick tests pass unchanged.Closes #293