nandi/gleanpublic Fork 0
8e6871d
Commits
Clone
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Parallelize database queries with errgroupUnverified

Julien Robert committed 2026-05-07T22:31:07+02:00 Browse files
8e6871d parent: c3ee647
modified internal/server/annotations_handler.go +50 -30
@@ -34,38 +34,58 @@ func (s *Server) handleLibrary(w http.ResponseWriter, r *http.Request) {
3434 likedPage := Pagination{Page: likedPageNum, PageSize: limit}
3535 annotPage := Pagination{Page: annotPageNum, PageSize: limit}
3636
37- articles, err := s.dbs.Articles.ListLikedArticles(ctx, user.DID, limit+1, likedPage.Offset())
38- if err != nil {
39- s.logger.Warn("failed to list liked articles", "error", err, "did", user.DID)
40- }
41- likedHasMore := len(articles) > limit
42- if likedHasMore {
43- articles = articles[:limit]
44- }
45- likedPage = likedPage.Paginate(len(articles))
46- if likedHasMore {
47- likedPage.HasNext = true
48- likedPage.NextPage = likedPage.Page + 1
49- }
37+ var (
38+ articles []*db.Article
39+ annotations []*db.Annotation
40+ )
5041
51- navSuffix := buildNavSuffix("", true)
52- for _, a := range articles {
53- a.NavSuffix = navSuffix
54- }
42+ g, gCtx := errgroup.WithContext(ctx)
5543
56- annotations, err := s.dbs.Articles.ListAnnotations(ctx, "", "", user.DID, limit+1, annotPage.Offset())
57- if err != nil {
58- s.logger.Warn("failed to list annotations", "error", err, "did", user.DID)
59- }
60- resolveAnnotationHandles(ctx, annotations)
61- annotHasMore := len(annotations) > limit
62- if annotHasMore {
63- annotations = annotations[:limit]
64- }
65- annotPage = annotPage.Paginate(len(annotations))
66- if annotHasMore {
67- annotPage.HasNext = true
68- annotPage.NextPage = annotPage.Page + 1
44+ g.Go(func() error {
45+ var err error
46+ articles, err = s.dbs.Articles.ListLikedArticles(gCtx, user.DID, limit+1, likedPage.Offset())
47+ if err != nil {
48+ s.logger.Warn("failed to list liked articles", "error", err, "did", user.DID)
49+ return nil
50+ }
51+ likedHasMore := len(articles) > limit
52+ if likedHasMore {
53+ articles = articles[:limit]
54+ }
55+ likedPage = likedPage.Paginate(len(articles))
56+ if likedHasMore {
57+ likedPage.HasNext = true
58+ likedPage.NextPage = likedPage.Page + 1
59+ }
60+ navSuffix := buildNavSuffix("", true)
61+ for _, a := range articles {
62+ a.NavSuffix = navSuffix
63+ }
64+ return nil
65+ })
66+
67+ g.Go(func() error {
68+ var err error
69+ annotations, err = s.dbs.Articles.ListAnnotations(gCtx, "", "", user.DID, limit+1, annotPage.Offset())
70+ if err != nil {
71+ s.logger.Warn("failed to list annotations", "error", err, "did", user.DID)
72+ return nil
73+ }
74+ resolveAnnotationHandles(gCtx, annotations)
75+ annotHasMore := len(annotations) > limit
76+ if annotHasMore {
77+ annotations = annotations[:limit]
78+ }
79+ annotPage = annotPage.Paginate(len(annotations))
80+ if annotHasMore {
81+ annotPage.HasNext = true
82+ annotPage.NextPage = annotPage.Page + 1
83+ }
84+ return nil
85+ })
86+
87+ if err := g.Wait(); err != nil {
88+ s.logger.Warn("library error", "error", err, "did", user.DID)
6989 }
7090
7191 s.render(w, r, "library.html", map[string]any{
@@ -34,38 +34,58 @@ func (s *Server) handleLibrary(w http.ResponseWriter, r *http.Request) {
34 likedPage := Pagination{Page: likedPageNum, PageSize: limit}34 likedPage := Pagination{Page: likedPageNum, PageSize: limit}
35 annotPage := Pagination{Page: annotPageNum, PageSize: limit}35 annotPage := Pagination{Page: annotPageNum, PageSize: limit}
36 36
37- articles, err := s.dbs.Articles.ListLikedArticles(ctx, user.DID, limit+1, likedPage.Offset())37+ var (
38- if err != nil {38+ articles []*db.Article
39- s.logger.Warn("failed to list liked articles", "error", err, "did", user.DID)39+ annotations []*db.Annotation
40- }40+ )
41- likedHasMore := len(articles) > limit
42- if likedHasMore {
43- articles = articles[:limit]
44- }
45- likedPage = likedPage.Paginate(len(articles))
46- if likedHasMore {
47- likedPage.HasNext = true
48- likedPage.NextPage = likedPage.Page + 1
49- }
50 41
51- navSuffix := buildNavSuffix("", true)42+ g, gCtx := errgroup.WithContext(ctx)
52- for _, a := range articles {
53- a.NavSuffix = navSuffix
54- }
55 43
56- annotations, err := s.dbs.Articles.ListAnnotations(ctx, "", "", user.DID, limit+1, annotPage.Offset())44+ g.Go(func() error {
57- if err != nil {45+ var err error
58- s.logger.Warn("failed to list annotations", "error", err, "did", user.DID)46+ articles, err = s.dbs.Articles.ListLikedArticles(gCtx, user.DID, limit+1, likedPage.Offset())
59- }47+ if err != nil {
60- resolveAnnotationHandles(ctx, annotations)48+ s.logger.Warn("failed to list liked articles", "error", err, "did", user.DID)
61- annotHasMore := len(annotations) > limit49+ return nil
62- if annotHasMore {50+ }
63- annotations = annotations[:limit]51+ likedHasMore := len(articles) > limit
64- }52+ if likedHasMore {
65- annotPage = annotPage.Paginate(len(annotations))53+ articles = articles[:limit]
66- if annotHasMore {54+ }
67- annotPage.HasNext = true55+ likedPage = likedPage.Paginate(len(articles))
68- annotPage.NextPage = annotPage.Page + 156+ if likedHasMore {
57+ likedPage.HasNext = true
58+ likedPage.NextPage = likedPage.Page + 1
59+ }
60+ navSuffix := buildNavSuffix("", true)
61+ for _, a := range articles {
62+ a.NavSuffix = navSuffix
63+ }
64+ return nil
65+ })
66+
67+ g.Go(func() error {
68+ var err error
69+ annotations, err = s.dbs.Articles.ListAnnotations(gCtx, "", "", user.DID, limit+1, annotPage.Offset())
70+ if err != nil {
71+ s.logger.Warn("failed to list annotations", "error", err, "did", user.DID)
72+ return nil
73+ }
74+ resolveAnnotationHandles(gCtx, annotations)
75+ annotHasMore := len(annotations) > limit
76+ if annotHasMore {
77+ annotations = annotations[:limit]
78+ }
79+ annotPage = annotPage.Paginate(len(annotations))
80+ if annotHasMore {
81+ annotPage.HasNext = true
82+ annotPage.NextPage = annotPage.Page + 1
83+ }
84+ return nil
85+ })
86+
87+ if err := g.Wait(); err != nil {
88+ s.logger.Warn("library error", "error", err, "did", user.DID)
69 }89 }
70 90
71 s.render(w, r, "library.html", map[string]any{91 s.render(w, r, "library.html", map[string]any{
modified internal/server/articles_handler.go +74 -32
@@ -10,6 +10,7 @@ import (
1010 "time"
1111
1212 "github.com/go-chi/chi/v5"
13+ "golang.org/x/sync/errgroup"
1314
1415 "pkg.rbrt.fr/glean/internal/atproto"
1516 "pkg.rbrt.fr/glean/internal/db"
@@ -165,48 +166,89 @@ func (s *Server) handleArticleDetail(w http.ResponseWriter, r *http.Request) {
165166 return
166167 }
167168
168- if err := s.dbs.Articles.MarkArticleRead(ctx, user.DID, id); err != nil {
169- s.logger.Warn("failed to mark article read", "error", err, "id", id)
170- }
169+ var (
170+ readState *db.ReadState
171+ likeCount int
172+ liked bool
173+ annotations []*db.Annotation
174+ feed *db.Feed
175+ nextID *int64
176+ )
171177
172- readState, err := s.dbs.Articles.GetReadState(ctx, user.DID, id)
173- if err != nil {
174- s.logger.Warn("failed to get read state", "error", err, "id", id)
175- }
178+ fromFeedURL := r.URL.Query().Get("from_feed")
179+ navLiked := r.URL.Query().Get("liked") == "1"
176180
177- var likeCount int
178- if article.URL.Valid {
179- likeCount, err = s.dbs.Articles.GetLikeCount(ctx, article.FeedURL, article.URL.String)
180- if err != nil {
181- s.logger.Warn("failed to get like count", "error", err, "feed", article.FeedURL)
181+ g, gCtx := errgroup.WithContext(ctx)
182+
183+ g.Go(func() error {
184+ if err := s.dbs.Articles.MarkArticleRead(gCtx, user.DID, id); err != nil {
185+ s.logger.Warn("failed to mark article read", "error", err, "id", id)
182186 }
183- }
187+ return nil
188+ })
184189
185- liked := false
186- if article.URL.Valid {
187- liked, err = s.dbs.Articles.HasLiked(ctx, user.DID, article.FeedURL, article.URL.String)
190+ g.Go(func() error {
191+ var err error
192+ readState, err = s.dbs.Articles.GetReadState(gCtx, user.DID, id)
188193 if err != nil {
189- s.logger.Warn("failed to check if liked", "error", err)
194+ s.logger.Warn("failed to get read state", "error", err, "id", id)
190195 }
191- }
196+ return nil
197+ })
192198
193- annotations, err := s.dbs.Articles.ListAnnotations(ctx, "", article.URL.String, "", 20, 0)
194- if err != nil {
195- s.logger.Warn("failed to list annotations", "error", err)
196- }
197- resolveAnnotationHandles(ctx, annotations)
199+ g.Go(func() error {
200+ if article.URL.Valid {
201+ var err error
202+ likeCount, err = s.dbs.Articles.GetLikeCount(gCtx, article.FeedURL, article.URL.String)
203+ if err != nil {
204+ s.logger.Warn("failed to get like count", "error", err, "feed", article.FeedURL)
205+ }
206+ }
207+ return nil
208+ })
198209
199- feed, err := s.dbs.Articles.GetFeed(ctx, article.FeedURL)
200- if err != nil {
201- s.logger.Warn("failed to get feed", "error", err, "feed", article.FeedURL)
202- }
210+ g.Go(func() error {
211+ if article.URL.Valid {
212+ var err error
213+ liked, err = s.dbs.Articles.HasLiked(gCtx, user.DID, article.FeedURL, article.URL.String)
214+ if err != nil {
215+ s.logger.Warn("failed to check if liked", "error", err)
216+ }
217+ }
218+ return nil
219+ })
203220
204- fromFeedURL := r.URL.Query().Get("from_feed")
205- navLiked := r.URL.Query().Get("liked") == "1"
221+ g.Go(func() error {
222+ var err error
223+ annotations, err = s.dbs.Articles.ListAnnotations(gCtx, "", article.URL.String, "", 20, 0)
224+ if err != nil {
225+ s.logger.Warn("failed to list annotations", "error", err)
226+ return nil
227+ }
228+ resolveAnnotationHandles(gCtx, annotations)
229+ return nil
230+ })
206231
207- nextID, err := s.dbs.Articles.GetNextArticleID(ctx, user.DID, id, fromFeedURL, navLiked)
208- if err != nil {
209- s.logger.Warn("failed to get next article", "error", err, "id", id)
232+ g.Go(func() error {
233+ var err error
234+ feed, err = s.dbs.Articles.GetFeed(gCtx, article.FeedURL)
235+ if err != nil {
236+ s.logger.Warn("failed to get feed", "error", err, "feed", article.FeedURL)
237+ }
238+ return nil
239+ })
240+
241+ g.Go(func() error {
242+ var err error
243+ nextID, err = s.dbs.Articles.GetNextArticleID(gCtx, user.DID, id, fromFeedURL, navLiked)
244+ if err != nil {
245+ s.logger.Warn("failed to get next article", "error", err, "id", id)
246+ }
247+ return nil
248+ })
249+
250+ if err := g.Wait(); err != nil {
251+ s.logger.Warn("article detail error", "error", err, "id", id)
210252 }
211253
212254 s.render(w, r, "article_detail.html", map[string]any{
@@ -10,6 +10,7 @@ import (
10 "time"10 "time"
11 11
12 "github.com/go-chi/chi/v5"12 "github.com/go-chi/chi/v5"
13+ "golang.org/x/sync/errgroup"
13 14
14 "pkg.rbrt.fr/glean/internal/atproto"15 "pkg.rbrt.fr/glean/internal/atproto"
15 "pkg.rbrt.fr/glean/internal/db"16 "pkg.rbrt.fr/glean/internal/db"
@@ -165,48 +166,89 @@ func (s *Server) handleArticleDetail(w http.ResponseWriter, r *http.Request) {
165 return166 return
166 }167 }
167 168
168- if err := s.dbs.Articles.MarkArticleRead(ctx, user.DID, id); err != nil {169+ var (
169- s.logger.Warn("failed to mark article read", "error", err, "id", id)170+ readState *db.ReadState
170- }171+ likeCount int
172+ liked bool
173+ annotations []*db.Annotation
174+ feed *db.Feed
175+ nextID *int64
176+ )
171 177
172- readState, err := s.dbs.Articles.GetReadState(ctx, user.DID, id)178+ fromFeedURL := r.URL.Query().Get("from_feed")
173- if err != nil {179+ navLiked := r.URL.Query().Get("liked") == "1"
174- s.logger.Warn("failed to get read state", "error", err, "id", id)
175- }
176 180
177- var likeCount int181+ g, gCtx := errgroup.WithContext(ctx)
178- if article.URL.Valid {182+
179- likeCount, err = s.dbs.Articles.GetLikeCount(ctx, article.FeedURL, article.URL.String)183+ g.Go(func() error {
180- if err != nil {184+ if err := s.dbs.Articles.MarkArticleRead(gCtx, user.DID, id); err != nil {
181- s.logger.Warn("failed to get like count", "error", err, "feed", article.FeedURL)185+ s.logger.Warn("failed to mark article read", "error", err, "id", id)
182 }186 }
183- }187+ return nil
188+ })
184 189
185- liked := false190+ g.Go(func() error {
186- if article.URL.Valid {191+ var err error
187- liked, err = s.dbs.Articles.HasLiked(ctx, user.DID, article.FeedURL, article.URL.String)192+ readState, err = s.dbs.Articles.GetReadState(gCtx, user.DID, id)
188 if err != nil {193 if err != nil {
189- s.logger.Warn("failed to check if liked", "error", err)194+ s.logger.Warn("failed to get read state", "error", err, "id", id)
190 }195 }
191- }196+ return nil
197+ })
192 198
193- annotations, err := s.dbs.Articles.ListAnnotations(ctx, "", article.URL.String, "", 20, 0)199+ g.Go(func() error {
194- if err != nil {200+ if article.URL.Valid {
195- s.logger.Warn("failed to list annotations", "error", err)201+ var err error
196- }202+ likeCount, err = s.dbs.Articles.GetLikeCount(gCtx, article.FeedURL, article.URL.String)
197- resolveAnnotationHandles(ctx, annotations)203+ if err != nil {
204+ s.logger.Warn("failed to get like count", "error", err, "feed", article.FeedURL)
205+ }
206+ }
207+ return nil
208+ })
198 209
199- feed, err := s.dbs.Articles.GetFeed(ctx, article.FeedURL)210+ g.Go(func() error {
200- if err != nil {211+ if article.URL.Valid {
201- s.logger.Warn("failed to get feed", "error", err, "feed", article.FeedURL)212+ var err error
202- }213+ liked, err = s.dbs.Articles.HasLiked(gCtx, user.DID, article.FeedURL, article.URL.String)
214+ if err != nil {
215+ s.logger.Warn("failed to check if liked", "error", err)
216+ }
217+ }
218+ return nil
219+ })
203 220
204- fromFeedURL := r.URL.Query().Get("from_feed")221+ g.Go(func() error {
205- navLiked := r.URL.Query().Get("liked") == "1"222+ var err error
223+ annotations, err = s.dbs.Articles.ListAnnotations(gCtx, "", article.URL.String, "", 20, 0)
224+ if err != nil {
225+ s.logger.Warn("failed to list annotations", "error", err)
226+ return nil
227+ }
228+ resolveAnnotationHandles(gCtx, annotations)
229+ return nil
230+ })
206 231
207- nextID, err := s.dbs.Articles.GetNextArticleID(ctx, user.DID, id, fromFeedURL, navLiked)232+ g.Go(func() error {
208- if err != nil {233+ var err error
209- s.logger.Warn("failed to get next article", "error", err, "id", id)234+ feed, err = s.dbs.Articles.GetFeed(gCtx, article.FeedURL)
235+ if err != nil {
236+ s.logger.Warn("failed to get feed", "error", err, "feed", article.FeedURL)
237+ }
238+ return nil
239+ })
240+
241+ g.Go(func() error {
242+ var err error
243+ nextID, err = s.dbs.Articles.GetNextArticleID(gCtx, user.DID, id, fromFeedURL, navLiked)
244+ if err != nil {
245+ s.logger.Warn("failed to get next article", "error", err, "id", id)
246+ }
247+ return nil
248+ })
249+
250+ if err := g.Wait(); err != nil {
251+ s.logger.Warn("article detail error", "error", err, "id", id)
210 }252 }
211 253
212 s.render(w, r, "article_detail.html", map[string]any{254 s.render(w, r, "article_detail.html", map[string]any{
modified internal/server/dashboard_handler.go +111 -45
@@ -9,52 +9,130 @@ import (
99
1010 "pkg.rbrt.fr/glean/internal/atproto"
1111 "pkg.rbrt.fr/glean/internal/cluster"
12+ "pkg.rbrt.fr/glean/internal/db"
1213 )
1314
1415 func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
1516 user := currentUser(r)
1617 ctx := r.Context()
1718
18- unreadCount, err := s.dbs.Articles.GetUnreadCount(ctx, user.DID, "")
19- if err != nil {
20- s.logger.Warn("failed to get unread count", "error", err, "did", user.DID)
21- }
22-
23- subCount, err := s.dbs.Articles.GetSubscriptionCount(ctx, user.DID)
24- if err != nil {
25- s.logger.Warn("failed to get subscription count", "error", err, "did", user.DID)
26- }
19+ var (
20+ unreadCount int
21+ subCount int
22+ userLangs []string
23+ articles []*db.Article
24+ articleRecs []*cluster.ArticleRecommendation
25+ peopleRecs []*cluster.PersonRecommendation
26+ feedRecs []*cluster.FeedRecommendation
27+ personalTrending []*db.TrendingItem
28+ globalTrending []*db.TrendingItem
29+ )
2730
2831 page := pageFromRequest(r, 25)
29- articles, err := s.dbs.Articles.ListUnreadArticles(ctx, user.DID, "", page.Limit()+1, page.Offset())
30- if err != nil {
31- s.logger.Warn("failed to list unread articles", "error", err, "did", user.DID)
32- }
33- totalFetched := len(articles)
34- page = page.Paginate(totalFetched)
35- if page.HasNext {
36- articles = articles[:page.PageSize]
37- }
32+ since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339)
3833
39- userLangs, err := s.dbs.Users.GetLanguages(ctx, user.DID)
40- if err != nil {
41- s.logger.Warn("failed to get user languages", "error", err, "did", user.DID)
42- }
34+ g, gCtx := errgroup.WithContext(ctx)
4335
44- articleRecs, err := s.engine.GetArticleRecommendations(ctx, user.DID, userLangs, 5)
45- if err != nil {
46- s.logger.Warn("failed to get article recommendations", "error", err, "did", user.DID)
47- }
36+ g.Go(func() error {
37+ var err error
38+ unreadCount, err = s.dbs.Articles.GetUnreadCount(gCtx, user.DID, "")
39+ if err != nil {
40+ s.logger.Warn("failed to get unread count", "error", err, "did", user.DID)
41+ }
42+ return nil
43+ })
44+
45+ g.Go(func() error {
46+ var err error
47+ subCount, err = s.dbs.Articles.GetSubscriptionCount(gCtx, user.DID)
48+ if err != nil {
49+ s.logger.Warn("failed to get subscription count", "error", err, "did", user.DID)
50+ }
51+ return nil
52+ })
53+
54+ g.Go(func() error {
55+ var err error
56+ articles, err = s.dbs.Articles.ListUnreadArticles(gCtx, user.DID, "", page.Limit()+1, page.Offset())
57+ if err != nil {
58+ s.logger.Warn("failed to list unread articles", "error", err, "did", user.DID)
59+ return nil
60+ }
61+ totalFetched := len(articles)
62+ page = page.Paginate(totalFetched)
63+ if page.HasNext {
64+ articles = articles[:page.PageSize]
65+ }
66+ return nil
67+ })
68+
69+ g.Go(func() error {
70+ var err error
71+ userLangs, err = s.dbs.Users.GetLanguages(gCtx, user.DID)
72+ if err != nil {
73+ s.logger.Warn("failed to get user languages", "error", err, "did", user.DID)
74+ }
75+ return nil
76+ })
77+
78+ g.Go(func() error {
79+ var err error
80+ peopleRecs, err = s.engine.GetPeopleRecommendations(gCtx, user.DID, 5)
81+ if err != nil {
82+ s.logger.Warn("failed to get people recommendations", "error", err, "did", user.DID)
83+ }
84+ return nil
85+ })
86+
87+ g.Go(func() error {
88+ var err error
89+ feedRecs, err = s.engine.GetFeedRecommendations(gCtx, user.DID, 5)
90+ if err != nil {
91+ s.logger.Warn("failed to get feed recommendations", "error", err, "did", user.DID)
92+ }
93+ return nil
94+ })
95+
96+ g.Go(func() error {
97+ var err error
98+ globalTrending, err = s.dbs.Articles.ListTrendingArticles(gCtx, user.DID, since, 10, 0)
99+ if err != nil {
100+ s.logger.Warn("failed to list global trending", "error", err, "did", user.DID)
101+ }
102+ return nil
103+ })
48104
49- peopleRecs, err := s.engine.GetPeopleRecommendations(ctx, user.DID, 5)
50- if err != nil {
51- s.logger.Warn("failed to get people recommendations", "error", err, "did", user.DID)
105+ if err := g.Wait(); err != nil {
106+ s.logger.Warn("dashboard phase 1 error", "error", err, "did", user.DID)
52107 }
53- resolvePeopleHandles(ctx, peopleRecs)
54108
55- feedRecs, err := s.engine.GetFeedRecommendations(ctx, user.DID, 5)
56- if err != nil {
57- s.logger.Warn("failed to get feed recommendations", "error", err, "did", user.DID)
109+ g2, gCtx2 := errgroup.WithContext(ctx)
110+
111+ g2.Go(func() error {
112+ var err error
113+ articleRecs, err = s.engine.GetArticleRecommendations(gCtx2, user.DID, userLangs, 5)
114+ if err != nil {
115+ s.logger.Warn("failed to get article recommendations", "error", err, "did", user.DID)
116+ }
117+ return nil
118+ })
119+
120+ g2.Go(func() error {
121+ var err error
122+ personalTrending, err = s.dbs.Articles.ListTrendingArticlesForUser(gCtx2, user.DID, since, userLangs, 5, 0)
123+ if err != nil {
124+ s.logger.Warn("failed to list personal trending", "error", err, "did", user.DID)
125+ }
126+ return nil
127+ })
128+
129+ g2.Go(func() error {
130+ resolvePeopleHandles(gCtx2, peopleRecs)
131+ return nil
132+ })
133+
134+ if err := g2.Wait(); err != nil {
135+ s.logger.Warn("dashboard phase 2 error", "error", err, "did", user.DID)
58136 }
59137
60138 var impressions []cluster.Impression
@@ -70,18 +148,6 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
70148 }
71149 }
72150
73- since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339)
74-
75- personalTrending, err := s.dbs.Articles.ListTrendingArticlesForUser(ctx, user.DID, since, userLangs, 5, 0)
76- if err != nil {
77- s.logger.Warn("failed to list personal trending", "error", err, "did", user.DID)
78- }
79-
80- globalTrending, err := s.dbs.Articles.ListTrendingArticles(ctx, user.DID, since, 10, 0)
81- if err != nil {
82- s.logger.Warn("failed to list global trending", "error", err, "did", user.DID)
83- }
84-
85151 var followedPeople, discoverPeople []*cluster.PersonRecommendation
86152 for _, p := range peopleRecs {
87153 if p.IsFollowed {
@@ -9,52 +9,130 @@ import (
9 9
10 "pkg.rbrt.fr/glean/internal/atproto"10 "pkg.rbrt.fr/glean/internal/atproto"
11 "pkg.rbrt.fr/glean/internal/cluster"11 "pkg.rbrt.fr/glean/internal/cluster"
12+ "pkg.rbrt.fr/glean/internal/db"
12 )13 )
13 14
14 func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {15 func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
15 user := currentUser(r)16 user := currentUser(r)
16 ctx := r.Context()17 ctx := r.Context()
17 18
18- unreadCount, err := s.dbs.Articles.GetUnreadCount(ctx, user.DID, "")19+ var (
19- if err != nil {20+ unreadCount int
20- s.logger.Warn("failed to get unread count", "error", err, "did", user.DID)21+ subCount int
21- }22+ userLangs []string
22-23+ articles []*db.Article
23- subCount, err := s.dbs.Articles.GetSubscriptionCount(ctx, user.DID)24+ articleRecs []*cluster.ArticleRecommendation
24- if err != nil {25+ peopleRecs []*cluster.PersonRecommendation
25- s.logger.Warn("failed to get subscription count", "error", err, "did", user.DID)26+ feedRecs []*cluster.FeedRecommendation
26- }27+ personalTrending []*db.TrendingItem
28+ globalTrending []*db.TrendingItem
29+ )
27 30
28 page := pageFromRequest(r, 25)31 page := pageFromRequest(r, 25)
29- articles, err := s.dbs.Articles.ListUnreadArticles(ctx, user.DID, "", page.Limit()+1, page.Offset())32+ since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339)
30- if err != nil {
31- s.logger.Warn("failed to list unread articles", "error", err, "did", user.DID)
32- }
33- totalFetched := len(articles)
34- page = page.Paginate(totalFetched)
35- if page.HasNext {
36- articles = articles[:page.PageSize]
37- }
38 33
39- userLangs, err := s.dbs.Users.GetLanguages(ctx, user.DID)34+ g, gCtx := errgroup.WithContext(ctx)
40- if err != nil {
41- s.logger.Warn("failed to get user languages", "error", err, "did", user.DID)
42- }
43 35
44- articleRecs, err := s.engine.GetArticleRecommendations(ctx, user.DID, userLangs, 5)36+ g.Go(func() error {
45- if err != nil {37+ var err error
46- s.logger.Warn("failed to get article recommendations", "error", err, "did", user.DID)38+ unreadCount, err = s.dbs.Articles.GetUnreadCount(gCtx, user.DID, "")
47- }39+ if err != nil {
40+ s.logger.Warn("failed to get unread count", "error", err, "did", user.DID)
41+ }
42+ return nil
43+ })
44+
45+ g.Go(func() error {
46+ var err error
47+ subCount, err = s.dbs.Articles.GetSubscriptionCount(gCtx, user.DID)
48+ if err != nil {
49+ s.logger.Warn("failed to get subscription count", "error", err, "did", user.DID)
50+ }
51+ return nil
52+ })
53+
54+ g.Go(func() error {
55+ var err error
56+ articles, err = s.dbs.Articles.ListUnreadArticles(gCtx, user.DID, "", page.Limit()+1, page.Offset())
57+ if err != nil {
58+ s.logger.Warn("failed to list unread articles", "error", err, "did", user.DID)
59+ return nil
60+ }
61+ totalFetched := len(articles)
62+ page = page.Paginate(totalFetched)
63+ if page.HasNext {
64+ articles = articles[:page.PageSize]
65+ }
66+ return nil
67+ })
68+
69+ g.Go(func() error {
70+ var err error
71+ userLangs, err = s.dbs.Users.GetLanguages(gCtx, user.DID)
72+ if err != nil {
73+ s.logger.Warn("failed to get user languages", "error", err, "did", user.DID)
74+ }
75+ return nil
76+ })
77+
78+ g.Go(func() error {
79+ var err error
80+ peopleRecs, err = s.engine.GetPeopleRecommendations(gCtx, user.DID, 5)
81+ if err != nil {
82+ s.logger.Warn("failed to get people recommendations", "error", err, "did", user.DID)
83+ }
84+ return nil
85+ })
86+
87+ g.Go(func() error {
88+ var err error
89+ feedRecs, err = s.engine.GetFeedRecommendations(gCtx, user.DID, 5)
90+ if err != nil {
91+ s.logger.Warn("failed to get feed recommendations", "error", err, "did", user.DID)
92+ }
93+ return nil
94+ })
95+
96+ g.Go(func() error {
97+ var err error
98+ globalTrending, err = s.dbs.Articles.ListTrendingArticles(gCtx, user.DID, since, 10, 0)
99+ if err != nil {
100+ s.logger.Warn("failed to list global trending", "error", err, "did", user.DID)
101+ }
102+ return nil
103+ })
48 104
49- peopleRecs, err := s.engine.GetPeopleRecommendations(ctx, user.DID, 5)105+ if err := g.Wait(); err != nil {
50- if err != nil {106+ s.logger.Warn("dashboard phase 1 error", "error", err, "did", user.DID)
51- s.logger.Warn("failed to get people recommendations", "error", err, "did", user.DID)
52 }107 }
53- resolvePeopleHandles(ctx, peopleRecs)
54 108
55- feedRecs, err := s.engine.GetFeedRecommendations(ctx, user.DID, 5)109+ g2, gCtx2 := errgroup.WithContext(ctx)
56- if err != nil {110+
57- s.logger.Warn("failed to get feed recommendations", "error", err, "did", user.DID)111+ g2.Go(func() error {
112+ var err error
113+ articleRecs, err = s.engine.GetArticleRecommendations(gCtx2, user.DID, userLangs, 5)
114+ if err != nil {
115+ s.logger.Warn("failed to get article recommendations", "error", err, "did", user.DID)
116+ }
117+ return nil
118+ })
119+
120+ g2.Go(func() error {
121+ var err error
122+ personalTrending, err = s.dbs.Articles.ListTrendingArticlesForUser(gCtx2, user.DID, since, userLangs, 5, 0)
123+ if err != nil {
124+ s.logger.Warn("failed to list personal trending", "error", err, "did", user.DID)
125+ }
126+ return nil
127+ })
128+
129+ g2.Go(func() error {
130+ resolvePeopleHandles(gCtx2, peopleRecs)
131+ return nil
132+ })
133+
134+ if err := g2.Wait(); err != nil {
135+ s.logger.Warn("dashboard phase 2 error", "error", err, "did", user.DID)
58 }136 }
59 137
60 var impressions []cluster.Impression138 var impressions []cluster.Impression
@@ -70,18 +148,6 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
70 }148 }
71 }149 }
72 150
73- since := time.Now().AddDate(0, 0, -7).Format(time.RFC3339)
74-
75- personalTrending, err := s.dbs.Articles.ListTrendingArticlesForUser(ctx, user.DID, since, userLangs, 5, 0)
76- if err != nil {
77- s.logger.Warn("failed to list personal trending", "error", err, "did", user.DID)
78- }
79-
80- globalTrending, err := s.dbs.Articles.ListTrendingArticles(ctx, user.DID, since, 10, 0)
81- if err != nil {
82- s.logger.Warn("failed to list global trending", "error", err, "did", user.DID)
83- }
84-
85 var followedPeople, discoverPeople []*cluster.PersonRecommendation151 var followedPeople, discoverPeople []*cluster.PersonRecommendation
86 for _, p := range peopleRecs {152 for _, p := range peopleRecs {
87 if p.IsFollowed {153 if p.IsFollowed {
modified internal/server/feeds_handler.go +92 -36
@@ -22,50 +22,104 @@ func (s *Server) handleFeeds(w http.ResponseWriter, r *http.Request) {
2222 ctx := r.Context()
2323
2424 page := pageFromRequest(r, 50)
25- subs, err := s.dbs.Articles.ListSubscriptions(ctx, user.DID, category, page.Limit()+1, page.Offset())
26- if err != nil {
27- s.logger.Warn("failed to list subscriptions", "error", err, "did", user.DID)
28- }
29- totalFetched := len(subs)
30- page = page.Paginate(totalFetched)
31- if page.HasNext {
32- subs = subs[:page.PageSize]
33- }
3425
35- subCount, err := s.dbs.Articles.GetSubscriptionCount(ctx, user.DID)
36- if err != nil {
37- s.logger.Warn("failed to get subscription count", "error", err, "did", user.DID)
38- }
26+ var (
27+ subs []*db.Subscription
28+ subCount int
29+ feedRecs []*cluster.FeedRecommendation
30+ peopleRecs []*cluster.PersonRecommendation
31+ deadFeeds []*db.Feed
32+ categories []string
33+ )
3934
40- feedRecs, err := s.engine.GetFeedRecommendations(ctx, user.DID, 6)
41- if err != nil {
42- s.logger.Warn("failed to get feed recommendations", "error", err, "did", user.DID)
43- }
35+ g, gCtx := errgroup.WithContext(ctx)
4436
45- peopleRecs, err := s.engine.GetPeopleRecommendations(ctx, user.DID, 5)
46- if err != nil {
47- s.logger.Warn("failed to get people recommendations", "error", err, "did", user.DID)
48- }
49- resolvePeopleHandles(ctx, peopleRecs)
37+ g.Go(func() error {
38+ var err error
39+ subs, err = s.dbs.Articles.ListSubscriptions(gCtx, user.DID, category, page.Limit()+1, page.Offset())
40+ if err != nil {
41+ s.logger.Warn("failed to list subscriptions", "error", err, "did", user.DID)
42+ return nil
43+ }
44+ totalFetched := len(subs)
45+ page = page.Paginate(totalFetched)
46+ if page.HasNext {
47+ subs = subs[:page.PageSize]
48+ }
49+ return nil
50+ })
5051
51- if len(feedRecs) > 0 {
52- impressions := make([]cluster.Impression, len(feedRecs))
53- for i, rec := range feedRecs {
54- impressions[i] = cluster.Impression{TargetType: "feed", TargetID: rec.FeedURL}
52+ g.Go(func() error {
53+ var err error
54+ subCount, err = s.dbs.Articles.GetSubscriptionCount(gCtx, user.DID)
55+ if err != nil {
56+ s.logger.Warn("failed to get subscription count", "error", err, "did", user.DID)
5557 }
56- if err := s.engine.RecordImpressions(ctx, user.DID, impressions); err != nil {
57- s.logger.Warn("failed to record impressions", "error", err)
58+ return nil
59+ })
60+
61+ g.Go(func() error {
62+ var err error
63+ feedRecs, err = s.engine.GetFeedRecommendations(gCtx, user.DID, 6)
64+ if err != nil {
65+ s.logger.Warn("failed to get feed recommendations", "error", err, "did", user.DID)
5866 }
59- }
67+ return nil
68+ })
6069
61- deadFeeds, err := s.dbs.Articles.ListDeadFeeds(ctx, user.DID, 7)
62- if err != nil {
63- s.logger.Warn("failed to list dead feeds", "error", err, "did", user.DID)
70+ g.Go(func() error {
71+ var err error
72+ peopleRecs, err = s.engine.GetPeopleRecommendations(gCtx, user.DID, 5)
73+ if err != nil {
74+ s.logger.Warn("failed to get people recommendations", "error", err, "did", user.DID)
75+ }
76+ return nil
77+ })
78+
79+ g.Go(func() error {
80+ var err error
81+ deadFeeds, err = s.dbs.Articles.ListDeadFeeds(gCtx, user.DID, 7)
82+ if err != nil {
83+ s.logger.Warn("failed to list dead feeds", "error", err, "did", user.DID)
84+ }
85+ return nil
86+ })
87+
88+ g.Go(func() error {
89+ var err error
90+ categories, err = s.dbs.Articles.GetCategories(gCtx, user.DID)
91+ if err != nil {
92+ s.logger.Warn("failed to get categories", "error", err, "did", user.DID)
93+ }
94+ return nil
95+ })
96+
97+ if err := g.Wait(); err != nil {
98+ s.logger.Warn("feeds phase 1 error", "error", err, "did", user.DID)
6499 }
65100
66- categories, err := s.dbs.Articles.GetCategories(ctx, user.DID)
67- if err != nil {
68- s.logger.Warn("failed to get categories", "error", err, "did", user.DID)
101+ g2, gCtx2 := errgroup.WithContext(ctx)
102+
103+ g2.Go(func() error {
104+ resolvePeopleHandles(gCtx2, peopleRecs)
105+ return nil
106+ })
107+
108+ g2.Go(func() error {
109+ if len(feedRecs) > 0 {
110+ impressions := make([]cluster.Impression, len(feedRecs))
111+ for i, rec := range feedRecs {
112+ impressions[i] = cluster.Impression{TargetType: "feed", TargetID: rec.FeedURL}
113+ }
114+ if err := s.engine.RecordImpressions(gCtx2, user.DID, impressions); err != nil {
115+ s.logger.Warn("failed to record impressions", "error", err)
116+ }
117+ }
118+ return nil
119+ })
120+
121+ if err := g2.Wait(); err != nil {
122+ s.logger.Warn("feeds phase 2 error", "error", err, "did", user.DID)
69123 }
70124
71125 var followedPeople, discoverPeople []*cluster.PersonRecommendation
@@ -349,7 +403,9 @@ func (s *Server) handleOPMLUpload(w http.ResponseWriter, r *http.Request) {
349403 return nil
350404 })
351405 }
352- _ = g.Wait()
406+ if err := g.Wait(); err != nil {
407+ s.logger.Warn("opml fetch error", "error", err)
408+ }
353409 }()
354410
355411 w.Header().Set("HX-Redirect", "/feeds")
@@ -22,50 +22,104 @@ func (s *Server) handleFeeds(w http.ResponseWriter, r *http.Request) {
22 ctx := r.Context()22 ctx := r.Context()
23 23
24 page := pageFromRequest(r, 50)24 page := pageFromRequest(r, 50)
25- subs, err := s.dbs.Articles.ListSubscriptions(ctx, user.DID, category, page.Limit()+1, page.Offset())
26- if err != nil {
27- s.logger.Warn("failed to list subscriptions", "error", err, "did", user.DID)
28- }
29- totalFetched := len(subs)
30- page = page.Paginate(totalFetched)
31- if page.HasNext {
32- subs = subs[:page.PageSize]
33- }
34 25
35- subCount, err := s.dbs.Articles.GetSubscriptionCount(ctx, user.DID)26+ var (
36- if err != nil {27+ subs []*db.Subscription
37- s.logger.Warn("failed to get subscription count", "error", err, "did", user.DID)28+ subCount int
38- }29+ feedRecs []*cluster.FeedRecommendation
30+ peopleRecs []*cluster.PersonRecommendation
31+ deadFeeds []*db.Feed
32+ categories []string
33+ )
39 34
40- feedRecs, err := s.engine.GetFeedRecommendations(ctx, user.DID, 6)35+ g, gCtx := errgroup.WithContext(ctx)
41- if err != nil {
42- s.logger.Warn("failed to get feed recommendations", "error", err, "did", user.DID)
43- }
44 36
45- peopleRecs, err := s.engine.GetPeopleRecommendations(ctx, user.DID, 5)37+ g.Go(func() error {
46- if err != nil {38+ var err error
47- s.logger.Warn("failed to get people recommendations", "error", err, "did", user.DID)39+ subs, err = s.dbs.Articles.ListSubscriptions(gCtx, user.DID, category, page.Limit()+1, page.Offset())
48- }40+ if err != nil {
49- resolvePeopleHandles(ctx, peopleRecs)41+ s.logger.Warn("failed to list subscriptions", "error", err, "did", user.DID)
42+ return nil
43+ }
44+ totalFetched := len(subs)
45+ page = page.Paginate(totalFetched)
46+ if page.HasNext {
47+ subs = subs[:page.PageSize]
48+ }
49+ return nil
50+ })
50 51
51- if len(feedRecs) > 0 {52+ g.Go(func() error {
52- impressions := make([]cluster.Impression, len(feedRecs))53+ var err error
53- for i, rec := range feedRecs {54+ subCount, err = s.dbs.Articles.GetSubscriptionCount(gCtx, user.DID)
54- impressions[i] = cluster.Impression{TargetType: "feed", TargetID: rec.FeedURL}55+ if err != nil {
56+ s.logger.Warn("failed to get subscription count", "error", err, "did", user.DID)
55 }57 }
56- if err := s.engine.RecordImpressions(ctx, user.DID, impressions); err != nil {58+ return nil
57- s.logger.Warn("failed to record impressions", "error", err)59+ })
60+
61+ g.Go(func() error {
62+ var err error
63+ feedRecs, err = s.engine.GetFeedRecommendations(gCtx, user.DID, 6)
64+ if err != nil {
65+ s.logger.Warn("failed to get feed recommendations", "error", err, "did", user.DID)
58 }66 }
59- }67+ return nil
68+ })
60 69
61- deadFeeds, err := s.dbs.Articles.ListDeadFeeds(ctx, user.DID, 7)70+ g.Go(func() error {
62- if err != nil {71+ var err error
63- s.logger.Warn("failed to list dead feeds", "error", err, "did", user.DID)72+ peopleRecs, err = s.engine.GetPeopleRecommendations(gCtx, user.DID, 5)
73+ if err != nil {
74+ s.logger.Warn("failed to get people recommendations", "error", err, "did", user.DID)
75+ }
76+ return nil
77+ })
78+
79+ g.Go(func() error {
80+ var err error
81+ deadFeeds, err = s.dbs.Articles.ListDeadFeeds(gCtx, user.DID, 7)
82+ if err != nil {
83+ s.logger.Warn("failed to list dead feeds", "error", err, "did", user.DID)
84+ }
85+ return nil
86+ })
87+
88+ g.Go(func() error {
89+ var err error
90+ categories, err = s.dbs.Articles.GetCategories(gCtx, user.DID)
91+ if err != nil {
92+ s.logger.Warn("failed to get categories", "error", err, "did", user.DID)
93+ }
94+ return nil
95+ })
96+
97+ if err := g.Wait(); err != nil {
98+ s.logger.Warn("feeds phase 1 error", "error", err, "did", user.DID)
64 }99 }
65 100
66- categories, err := s.dbs.Articles.GetCategories(ctx, user.DID)101+ g2, gCtx2 := errgroup.WithContext(ctx)
67- if err != nil {102+
68- s.logger.Warn("failed to get categories", "error", err, "did", user.DID)103+ g2.Go(func() error {
104+ resolvePeopleHandles(gCtx2, peopleRecs)
105+ return nil
106+ })
107+
108+ g2.Go(func() error {
109+ if len(feedRecs) > 0 {
110+ impressions := make([]cluster.Impression, len(feedRecs))
111+ for i, rec := range feedRecs {
112+ impressions[i] = cluster.Impression{TargetType: "feed", TargetID: rec.FeedURL}
113+ }
114+ if err := s.engine.RecordImpressions(gCtx2, user.DID, impressions); err != nil {
115+ s.logger.Warn("failed to record impressions", "error", err)
116+ }
117+ }
118+ return nil
119+ })
120+
121+ if err := g2.Wait(); err != nil {
122+ s.logger.Warn("feeds phase 2 error", "error", err, "did", user.DID)
69 }123 }
70 124
71 var followedPeople, discoverPeople []*cluster.PersonRecommendation125 var followedPeople, discoverPeople []*cluster.PersonRecommendation
@@ -349,7 +403,9 @@ func (s *Server) handleOPMLUpload(w http.ResponseWriter, r *http.Request) {
349 return nil403 return nil
350 })404 })
351 }405 }
352- _ = g.Wait()406+ if err := g.Wait(); err != nil {
407+ s.logger.Warn("opml fetch error", "error", err)
408+ }
353 }()409 }()
354 410
355 w.Header().Set("HX-Redirect", "/feeds")411 w.Header().Set("HX-Redirect", "/feeds")
modified internal/server/profile_handler.go +46 -15
@@ -5,8 +5,10 @@ import (
55 "strings"
66
77 "github.com/go-chi/chi/v5"
8+ "golang.org/x/sync/errgroup"
89
910 "pkg.rbrt.fr/glean/internal/atproto"
11+ "pkg.rbrt.fr/glean/internal/db"
1012 "pkg.rbrt.fr/glean/internal/langdetect"
1113 )
1214
@@ -39,25 +41,54 @@ func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) {
3941 profileUser.DisplayName = p.DisplayName
4042 profileUser.AvatarURL = p.AvatarURL
4143
42- subs, err := s.dbs.Articles.ListSubscriptions(ctx, did, "", 50, 0)
43- if err != nil {
44- s.logger.Warn("failed to list subscriptions", "error", err, "did", did)
45- }
44+ var (
45+ subs []*db.Subscription
46+ annotations []*db.Annotation
47+ subCount int
48+ userLangs []string
49+ )
4650
47- annotations, err := s.dbs.Articles.ListAnnotations(ctx, "", "", did, 50, 0)
48- if err != nil {
49- s.logger.Warn("failed to list annotations", "error", err, "did", did)
50- }
51- resolveAnnotationHandles(ctx, annotations)
51+ user := currentUser(r)
5252
53- subCount, err := s.dbs.Articles.GetSubscriptionCount(ctx, did)
54- if err != nil {
55- s.logger.Warn("failed to get subscription count", "error", err, "did", did)
56- }
53+ g, gCtx := errgroup.WithContext(ctx)
5754
58- user := currentUser(r)
55+ g.Go(func() error {
56+ var err error
57+ subs, err = s.dbs.Articles.ListSubscriptions(gCtx, did, "", 50, 0)
58+ if err != nil {
59+ s.logger.Warn("failed to list subscriptions", "error", err, "did", did)
60+ }
61+ return nil
62+ })
5963
60- userLangs, _ := s.dbs.Users.GetLanguages(ctx, user.DID)
64+ g.Go(func() error {
65+ var err error
66+ annotations, err = s.dbs.Articles.ListAnnotations(gCtx, "", "", did, 50, 0)
67+ if err != nil {
68+ s.logger.Warn("failed to list annotations", "error", err, "did", did)
69+ return nil
70+ }
71+ resolveAnnotationHandles(gCtx, annotations)
72+ return nil
73+ })
74+
75+ g.Go(func() error {
76+ var err error
77+ subCount, err = s.dbs.Articles.GetSubscriptionCount(gCtx, did)
78+ if err != nil {
79+ s.logger.Warn("failed to get subscription count", "error", err, "did", did)
80+ }
81+ return nil
82+ })
83+
84+ g.Go(func() error {
85+ userLangs, _ = s.dbs.Users.GetLanguages(gCtx, user.DID)
86+ return nil
87+ })
88+
89+ if err := g.Wait(); err != nil {
90+ s.logger.Warn("profile error", "error", err, "did", did)
91+ }
6192
6293 s.render(w, r, "profile.html", map[string]any{
6394 "User": user,
@@ -5,8 +5,10 @@ import (
5 "strings"5 "strings"
6 6
7 "github.com/go-chi/chi/v5"7 "github.com/go-chi/chi/v5"
8+ "golang.org/x/sync/errgroup"
8 9
9 "pkg.rbrt.fr/glean/internal/atproto"10 "pkg.rbrt.fr/glean/internal/atproto"
11+ "pkg.rbrt.fr/glean/internal/db"
10 "pkg.rbrt.fr/glean/internal/langdetect"12 "pkg.rbrt.fr/glean/internal/langdetect"
11 )13 )
12 14
@@ -39,25 +41,54 @@ func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) {
39 profileUser.DisplayName = p.DisplayName41 profileUser.DisplayName = p.DisplayName
40 profileUser.AvatarURL = p.AvatarURL42 profileUser.AvatarURL = p.AvatarURL
41 43
42- subs, err := s.dbs.Articles.ListSubscriptions(ctx, did, "", 50, 0)44+ var (
43- if err != nil {45+ subs []*db.Subscription
44- s.logger.Warn("failed to list subscriptions", "error", err, "did", did)46+ annotations []*db.Annotation
45- }47+ subCount int
48+ userLangs []string
49+ )
46 50
47- annotations, err := s.dbs.Articles.ListAnnotations(ctx, "", "", did, 50, 0)51+ user := currentUser(r)
48- if err != nil {
49- s.logger.Warn("failed to list annotations", "error", err, "did", did)
50- }
51- resolveAnnotationHandles(ctx, annotations)
52 52
53- subCount, err := s.dbs.Articles.GetSubscriptionCount(ctx, did)53+ g, gCtx := errgroup.WithContext(ctx)
54- if err != nil {
55- s.logger.Warn("failed to get subscription count", "error", err, "did", did)
56- }
57 54
58- user := currentUser(r)55+ g.Go(func() error {
56+ var err error
57+ subs, err = s.dbs.Articles.ListSubscriptions(gCtx, did, "", 50, 0)
58+ if err != nil {
59+ s.logger.Warn("failed to list subscriptions", "error", err, "did", did)
60+ }
61+ return nil
62+ })
59 63
60- userLangs, _ := s.dbs.Users.GetLanguages(ctx, user.DID)64+ g.Go(func() error {
65+ var err error
66+ annotations, err = s.dbs.Articles.ListAnnotations(gCtx, "", "", did, 50, 0)
67+ if err != nil {
68+ s.logger.Warn("failed to list annotations", "error", err, "did", did)
69+ return nil
70+ }
71+ resolveAnnotationHandles(gCtx, annotations)
72+ return nil
73+ })
74+
75+ g.Go(func() error {
76+ var err error
77+ subCount, err = s.dbs.Articles.GetSubscriptionCount(gCtx, did)
78+ if err != nil {
79+ s.logger.Warn("failed to get subscription count", "error", err, "did", did)
80+ }
81+ return nil
82+ })
83+
84+ g.Go(func() error {
85+ userLangs, _ = s.dbs.Users.GetLanguages(gCtx, user.DID)
86+ return nil
87+ })
88+
89+ if err := g.Wait(); err != nil {
90+ s.logger.Warn("profile error", "error", err, "did", did)
91+ }
61 92
62 s.render(w, r, "profile.html", map[string]any{93 s.render(w, r, "profile.html", map[string]any{
63 "User": user,94 "User": user,