fix: feed repitition

This commit is contained in:
Krispire Admin
2026-06-14 00:18:11 +05:30
parent 8a2bdf945a
commit 2afbbfa0f2
6 changed files with 263 additions and 11 deletions
+35 -3
View File
@@ -36,11 +36,12 @@ type PostService struct {
likes *repository.LikeRepo
mentions *repository.MentionRepo
comments *repository.CommentRepo
feedSeen *repository.FeedRepo // per-user "already served" set for the home feed
notif *NotificationService // optional; set via SetNotifier
}
func NewPostService(posts *repository.PostRepo, r2 *repository.R2, likes *repository.LikeRepo, mentions *repository.MentionRepo, comments *repository.CommentRepo) *PostService {
return &PostService{posts: posts, r2: r2, likes: likes, mentions: mentions, comments: comments}
func NewPostService(posts *repository.PostRepo, r2 *repository.R2, likes *repository.LikeRepo, mentions *repository.MentionRepo, comments *repository.CommentRepo, feedSeen *repository.FeedRepo) *PostService {
return &PostService{posts: posts, r2: r2, likes: likes, mentions: mentions, comments: comments, feedSeen: feedSeen}
}
// SetNotifier wires the notification seam after construction (the
@@ -224,7 +225,23 @@ func (s *PostService) Feed(ctx context.Context, viewerID string, limit, offset i
if offset < 0 {
offset = 0
}
posts, err := s.posts.FeedForViewer(ctx, viewerID, limit, offset)
// Non-repetition: a per-user Redis "seen" set is the cursor. offset==0 is
// the start of a fresh session, so we clear it; every page then excludes
// already-served ids. Because the feed's tier ordering depends on viewer
// state (a like re-tiers a post), exclusion — not OFFSET — is what keeps
// pages from repeating or skipping rows. With Redis enabled we drive
// selection by the set alone (offset 0); without it we fall back to plain
// offset paging and the client de-dups the occasional drift.
if offset == 0 {
s.feedSeen.Reset(ctx, viewerID)
}
exclude := s.feedSeen.Seen(ctx, viewerID)
queryOffset := offset
if s.feedSeen.Enabled() {
queryOffset = 0
}
posts, err := s.posts.FeedForViewer(ctx, viewerID, limit, queryOffset, exclude)
if err != nil {
return nil, err
}
@@ -243,9 +260,24 @@ func (s *PostService) Feed(ctx context.Context, viewerID string, limit, offset i
if err := s.posts.HydrateViewerRelationships(ctx, viewerID, posts); err != nil {
return nil, err
}
// Record what we served so the next page skips it (no-op without Redis).
if len(posts) > 0 {
ids := make([]string, len(posts))
for i := range posts {
ids[i] = posts[i].ID
}
s.feedSeen.MarkSeen(ctx, viewerID, ids)
}
return posts, nil
}
// ResetFeed clears the viewer's home-feed seen-set — called on logout and when
// the session ends (the page unloads / the realtime socket drops), so a stale
// set doesn't linger in Redis past its owner. No-op without Redis.
func (s *PostService) ResetFeed(ctx context.Context, viewerID string) {
s.feedSeen.Reset(ctx, viewerID)
}
// hydratePinnedSlice fills PinnedComment on a value slice.
func (s *PostService) hydratePinnedSlice(ctx context.Context, viewerID string, posts []model.Post) error {
if len(posts) == 0 {