summaryrefslogtreecommitdiff
path: root/server/internal
diff options
context:
space:
mode:
Diffstat (limited to 'server/internal')
-rw-r--r--server/internal/api/api.go6
-rw-r--r--server/internal/api/api_test.go14
-rw-r--r--server/internal/store/store.go2
-rw-r--r--server/internal/store/tier.go86
-rw-r--r--server/internal/store/tier_test.go34
-rw-r--r--server/internal/words/pipeline.go18
-rw-r--r--server/internal/words/pipeline_test.go8
7 files changed, 93 insertions, 75 deletions
diff --git a/server/internal/api/api.go b/server/internal/api/api.go
index 2d2e279..5a11c92 100644
--- a/server/internal/api/api.go
+++ b/server/internal/api/api.go
@@ -181,10 +181,10 @@ func (s *Server) subscriptions(w http.ResponseWriter, r *http.Request) {
}
var body struct {
Feeds []string `json:"feeds"`
- // Tiers says how soon each feed's work is worth doing, keyed by URL. Optional and
+ // Categories says what sort of listening each feed is, keyed by URL, which is also
// separate from the list rather than folded into it, so a phone that knows nothing
// about tiers keeps working unchanged.
- Tiers map[string]string `json:"tiers,omitempty"`
+ Categories map[string]string `json:"categories,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "expected {\"feeds\":[...]}"})
@@ -200,7 +200,7 @@ func (s *Server) subscriptions(w http.ResponseWriter, r *http.Request) {
return
}
// After the list, so a tier for a feed that was just removed is dropped with it.
- s.state.SetTiers(body.Tiers)
+ s.state.SetCategories(body.Categories)
if s.opts.OnSubscriptions != nil {
s.opts.OnSubscriptions(s.state.Feeds())
}
diff --git a/server/internal/api/api_test.go b/server/internal/api/api_test.go
index b50454b..5ae4435 100644
--- a/server/internal/api/api_test.go
+++ b/server/internal/api/api_test.go
@@ -379,21 +379,21 @@ Tiers arrive beside the list rather than folded into it, so a phone that knows n
them keeps working unchanged -- which every phone did until this week.
*/
-func TestTakesTheTiersThePhoneSends(t *testing.T) {
+func TestTakesTheCategoriesThePhoneSends(t *testing.T) {
s, st, _, _ := newServer(t)
post(t, s, "/api/subscriptions",
- `{"feeds":["https://a","https://b"],"tiers":{"https://a":"topical"}}`)
+ `{"feeds":["https://a","https://b"],"categories":{"https://a":"news"}}`)
- if got := st.Tier("https://a"); got != store.TierTopical {
- t.Errorf("got %q, want %q", got, store.TierTopical)
+ if got := st.Category("https://a"); got != store.CategoryNews {
+ t.Errorf("got %q, want %q", got, store.CategoryNews)
}
- if got := st.Tier("https://b"); got != store.TierOrdinary {
+ if got := st.Category("https://b"); got != store.CategoryDefault {
t.Errorf("got %q, want the default", got)
}
}
-func TestAListWithNoTiersStillWorks(t *testing.T) {
+func TestAListWithNoCategoriesStillWorks(t *testing.T) {
s, st, _, _ := newServer(t)
rec := post(t, s, "/api/subscriptions", `{"feeds":["https://a"]}`)
@@ -414,7 +414,7 @@ func TestATierForARemovedFeedIsDropped(t *testing.T) {
post(t, s, "/api/subscriptions", `{"feeds":["https://b"],"tiers":{"https://a":"archive"}}`)
- if got := st.Tier("https://a"); got != store.TierOrdinary {
+ if got := st.Category("https://a"); got != store.CategoryDefault {
t.Errorf("got %q, want it dropped with the feed", got)
}
}
diff --git a/server/internal/store/store.go b/server/internal/store/store.go
index de763ed..caf5365 100644
--- a/server/internal/store/store.go
+++ b/server/internal/store/store.go
@@ -469,7 +469,7 @@ func (s *State) SetFeeds(feeds []string) (removed []string) {
}
for url := range s.tiers {
if !wanted[url] {
- s.forgetTier(url)
+ s.forgetCategory(url)
}
}
sort.Strings(removed)
diff --git a/server/internal/store/tier.go b/server/internal/store/tier.go
index 94d2634..eb46aca 100644
--- a/server/internal/store/tier.go
+++ b/server/internal/store/tier.go
@@ -5,7 +5,7 @@ import "sort"
/*
How soon a show's work is worth doing.
-Peter's framing: a few shows are news and stop being worth hearing within a day, a tier he keeps
+Peter's framing: a few shows are news and stop being worth hearing within a day, a category he keeps
up with, and a long tail that is not topical at all. It matters because recognition costs about
five minutes of CPU an episode, so a five-hour Dan Carlin episode landing the same morning as Up
First takes most of a cycle's budget before the news is touched.
@@ -14,31 +14,55 @@ Three and no more. A number would invite fiddling and would not mean anything; t
three distinctions he actually described.
*/
const (
- // TierTopical is news: worth hearing today and worthless by Thursday.
- TierTopical = "topical"
- // TierOrdinary is a show kept up with. The default, because it is what most of a library
- // is and because a show nobody has placed should not be starved.
- TierOrdinary = "ordinary"
- // TierArchive is a back catalogue, a slow drip, anything heard whenever. Its work waits
- // for the time nothing else wants.
- TierArchive = "archive"
+ // The categories, in the order the work is done. The phone sends these; see
+ // docs/show-kinds.md in the app for what each means and why the order is what it is.
+ //
+ // They replaced three values that were priority alone -- topical, ordinary, archive --
+ // which could not tell the daily news from a daily horror serial. A category says what a
+ // show *is*, and the order of the list is the priority, so there is one thing to set
+ // rather than two that can disagree.
+ CategoryNews = "news"
+ CategoryAnalysis = "analysis"
+ CategoryCuriosity = "curiosity"
+ CategorySailing = "sailing"
+ CategoryTrue = "truestory"
+ CategoryFiction = "fiction"
+ CategoryDeep = "deep"
)
-// tierRank orders the work. Lower goes first.
-var tierRank = map[string]int{TierTopical: 0, TierOrdinary: 1, TierArchive: 2}
+// categoryRank orders the work. Lower goes first, and the order is the declaration order above.
+//
+// A category this server does not know -- sent by a newer phone -- ranks with the default
+// rather than first or last, so an unrecognised value neither jumps the queue nor starves.
+var categoryRank = map[string]int{
+ CategoryNews: 0, CategoryAnalysis: 1, CategoryCuriosity: 2, CategorySailing: 3,
+ CategoryTrue: 4, CategoryFiction: 5, CategoryDeep: 6,
+}
+
+// CategoryDefault is where an unplaced or unrecognised show sits: the middle of the order.
+const CategoryDefault = CategoryTrue
+
+// CategoryRank is where a category sits in the work order, for callers outside this package.
+// An unknown one ranks with the default rather than first or last.
+func CategoryRank(category string) int {
+ if rank, ok := categoryRank[category]; ok {
+ return rank
+ }
+ return categoryRank[CategoryDefault]
+}
/*
-SetTiers records where the phone placed each feed, replacing what was there.
+SetCategories records where the phone placed each feed, replacing what was there.
-Only for feeds that are actually subscribed. The phone sends its list and its tiers in one
-push, and a tier naming a feed the same push dropped would otherwise outlive it and be
+Only for feeds that are actually subscribed. The phone sends its list and its categories in one
+push, and a category naming a feed the same push dropped would otherwise outlive it and be
inherited by a resubscription months later.
-A tier this server does not understand is dropped rather than stored: a newer phone inventing a
+A category this server does not understand is dropped rather than stored: a newer phone inventing a
fourth must not quietly stop a show being polled, which is the rule the label store already
follows for a verdict it does not recognise.
*/
-func (s *State) SetTiers(tiers map[string]string) {
+func (s *State) SetCategories(categories map[string]string) {
s.mu.Lock()
defer s.mu.Unlock()
if s.tiers == nil {
@@ -48,38 +72,38 @@ func (s *State) SetTiers(tiers map[string]string) {
for _, url := range s.feeds {
subscribed[url] = true
}
- for url, tier := range tiers {
- _, known := tierRank[tier]
- if !subscribed[url] || !known || tier == TierOrdinary {
+ for url, category := range categories {
+ _, known := categoryRank[category]
+ if !subscribed[url] || !known || category == CategoryDefault {
delete(s.tiers, url)
continue
}
- s.tiers[url] = tier
+ s.tiers[url] = category
}
}
-// Tier is where a feed sits, or [TierOrdinary].
-func (s *State) Tier(feedURL string) string {
+// Category is where a feed sits, or [CategoryDefault].
+func (s *State) Category(feedURL string) string {
s.mu.RLock()
defer s.mu.RUnlock()
- if tier, ok := s.tiers[feedURL]; ok {
- return tier
+ if category, ok := s.tiers[feedURL]; ok {
+ return category
}
- return TierOrdinary
+ return CategoryDefault
}
-// ByTier orders feed URLs so the topical ones are worked first and the archive last.
+// ByCategory orders feed URLs so the news is worked first and the deep dives last.
//
-// Stable, so an ordering within a tier -- newest first, usually -- survives being grouped. A
+// Stable, so an ordering within a category -- newest first, usually -- survives being grouped. A
// sort that reshuffled equals would make the queue unpredictable between cycles for no gain.
-func (s *State) ByTier(feedURLs []string) []string {
+func (s *State) ByCategory(feedURLs []string) []string {
out := append([]string(nil), feedURLs...)
sort.SliceStable(out, func(i, j int) bool {
- return tierRank[s.Tier(out[i])] < tierRank[s.Tier(out[j])]
+ return categoryRank[s.Category(out[i])] < categoryRank[s.Category(out[j])]
})
return out
}
-// forgetTier drops a feed's placement. Called when the feed goes, or a feed resubscribed months
+// forgetCategory drops a feed's placement. Called when the feed goes, or a feed resubscribed months
// later would arrive carrying a placement nobody chose.
-func (s *State) forgetTier(feedURL string) { delete(s.tiers, feedURL) }
+func (s *State) forgetCategory(feedURL string) { delete(s.tiers, feedURL) }
diff --git a/server/internal/store/tier_test.go b/server/internal/store/tier_test.go
index 971ee51..025f323 100644
--- a/server/internal/store/tier_test.go
+++ b/server/internal/store/tier_test.go
@@ -17,10 +17,10 @@ the three distinctions he actually described.
func TestATierIsRememberedForAFeed(t *testing.T) {
s := New()
s.SetFeeds([]string{"https://a", "https://b"})
- s.SetTiers(map[string]string{"https://a": TierTopical})
+ s.SetCategories(map[string]string{"https://a": CategoryNews})
- if got := s.Tier("https://a"); got != TierTopical {
- t.Errorf("got %q, want %q", got, TierTopical)
+ if got := s.Category("https://a"); got != CategoryNews {
+ t.Errorf("got %q, want %q", got, CategoryNews)
}
}
@@ -29,8 +29,8 @@ func TestAShowWithNoTierIsOrdinary(t *testing.T) {
s := New()
s.SetFeeds([]string{"https://a"})
- if got := s.Tier("https://a"); got != TierOrdinary {
- t.Errorf("got %q, want %q", got, TierOrdinary)
+ if got := s.Category("https://a"); got != CategoryDefault {
+ t.Errorf("got %q, want %q", got, CategoryDefault)
}
}
@@ -43,9 +43,9 @@ same rule the label store already follows for a verdict it does not recognise.
func TestAnUnknownTierIsIgnored(t *testing.T) {
s := New()
s.SetFeeds([]string{"https://a"})
- s.SetTiers(map[string]string{"https://a": "urgent-ish"})
+ s.SetCategories(map[string]string{"https://a": "urgent-ish"})
- if got := s.Tier("https://a"); got != TierOrdinary {
+ if got := s.Category("https://a"); got != CategoryDefault {
t.Errorf("got %q, want the ordinary tier", got)
}
}
@@ -55,10 +55,10 @@ func TestAnUnknownTierIsIgnored(t *testing.T) {
func TestForgettingAFeedForgetsItsTier(t *testing.T) {
s := New()
s.SetFeeds([]string{"https://a"})
- s.SetTiers(map[string]string{"https://a": TierTopical})
+ s.SetCategories(map[string]string{"https://a": CategoryNews})
s.SetFeeds([]string{"https://b"})
- if got := s.Tier("https://a"); got != TierOrdinary {
+ if got := s.Category("https://a"); got != CategoryDefault {
t.Errorf("got %q, want the tier forgotten with the feed", got)
}
}
@@ -69,16 +69,16 @@ Ordering runs topical first and archive last, and keeps the given order within a
Stable so that "newest first" survives being grouped: an ordering that reshuffled equals would
make the queue unpredictable from one cycle to the next for no gain.
*/
-func TestWorkIsOrderedByTier(t *testing.T) {
+func TestWorkIsOrderedByCategory(t *testing.T) {
s := New()
s.SetFeeds([]string{"https://news", "https://usual", "https://archive", "https://news2"})
- s.SetTiers(map[string]string{
- "https://news": TierTopical,
- "https://news2": TierTopical,
- "https://archive": TierArchive,
+ s.SetCategories(map[string]string{
+ "https://news": CategoryNews,
+ "https://news2": CategoryNews,
+ "https://archive": CategoryDeep,
})
- got := s.ByTier([]string{"https://usual", "https://archive", "https://news", "https://news2"})
+ got := s.ByCategory([]string{"https://usual", "https://archive", "https://news", "https://news2"})
want := []string{"https://news", "https://news2", "https://usual", "https://archive"}
for i := range want {
if got[i] != want[i] {
@@ -96,9 +96,9 @@ dropped would outlive it and be inherited by a resubscription months later.
func TestATierForAFeedNotSubscribedIsDropped(t *testing.T) {
s := New()
s.SetFeeds([]string{"https://b"})
- s.SetTiers(map[string]string{"https://a": TierArchive})
+ s.SetCategories(map[string]string{"https://a": CategoryDeep})
- if got := s.Tier("https://a"); got != TierOrdinary {
+ if got := s.Category("https://a"); got != CategoryDefault {
t.Errorf("got %q, want nothing kept for an unsubscribed feed", got)
}
}
diff --git a/server/internal/words/pipeline.go b/server/internal/words/pipeline.go
index 8cd18ed..d8de8d8 100644
--- a/server/internal/words/pipeline.go
+++ b/server/internal/words/pipeline.go
@@ -102,17 +102,11 @@ func (p *Pipeline) Forget(e *store.Episode) {
e.WordBreaks, e.WordAdsRefused = nil, ""
}
-// tierRankOf orders the tiers for work: topical first, archive last.
-func tierRankOf(tier string) int {
- switch tier {
- case store.TierTopical:
- return 0
- case store.TierArchive:
- return 2
- default:
- return 1
- }
-}
+// categoryRankOf orders the categories for work, news first and deep dives last.
+//
+// Delegates rather than keeping its own table: the order is the category list's declaration
+// order, and two copies of it would drift the first time one is added.
+func categoryRankOf(category string) int { return store.CategoryRank(category) }
// clock renders a duration the way every readout in this project does.
func clock(ms int64) string {
@@ -725,7 +719,7 @@ func (p *Pipeline) pendingRecognition() []*store.Episode {
rank := map[string]int{}
for _, show := range p.State.Shows() {
for _, e := range show.Episodes {
- rank[e.ID] = tierRankOf(p.State.Tier(show.FeedURL))
+ rank[e.ID] = categoryRankOf(p.State.Category(show.FeedURL))
}
}
sort.SliceStable(pending, func(i, j int) bool {
diff --git a/server/internal/words/pipeline_test.go b/server/internal/words/pipeline_test.go
index 9982a10..cf23d8f 100644
--- a/server/internal/words/pipeline_test.go
+++ b/server/internal/words/pipeline_test.go
@@ -76,13 +76,13 @@ A five-hour back-catalogue episode arriving the same morning as the news would o
most of a cycle's recognition budget before the news was looked at.
*/
func TestTheWorkOrderPutsTopicalShowsFirst(t *testing.T) {
- if tierRankOf(store.TierTopical) >= tierRankOf(store.TierOrdinary) {
+ if categoryRankOf(store.CategoryNews) >= categoryRankOf(store.CategoryDefault) {
t.Error("a topical show does not come first")
}
- if tierRankOf(store.TierArchive) <= tierRankOf(store.TierOrdinary) {
+ if categoryRankOf(store.CategoryDeep) <= categoryRankOf(store.CategoryDefault) {
t.Error("an archive show does not come last")
}
- if tierRankOf("") != tierRankOf(store.TierOrdinary) {
+ if categoryRankOf("") != categoryRankOf(store.CategoryDefault) {
t.Error("a show nobody placed is not treated as ordinary, so it could be starved")
}
}
@@ -90,7 +90,7 @@ func TestTheWorkOrderPutsTopicalShowsFirst(t *testing.T) {
func TestRecognitionIsOrderedByTierThenByWhatChecksItself(t *testing.T) {
state := store.New()
state.SetFeeds([]string{"https://news", "https://usual"})
- state.SetTiers(map[string]string{"https://news": store.TierTopical})
+ state.SetCategories(map[string]string{"https://news": store.CategoryNews})
state.Merge("https://usual", "Usual", nil)
state.Merge("https://news", "News", nil)