package repository import ( "context" "time" "go.uber.org/zap" ) // feedSeenTTL bounds a home-feed "session": after this idle window the // per-user seen-set expires and the feed starts fresh from the top. const feedSeenTTL = 30 * time.Minute // FeedRepo tracks, per user, which post ids have already been served in the // current home-feed session (a Redis SET) so paginated requests never repeat // a post — and so the viewer-state-dependent feed ordering can't cause an // OFFSET to drift over already-served rows. // // Nil-safe: with no Redis configured every method degrades to a no-op / // empty, and the feed falls back to plain offset paging. type FeedRepo struct { redis *Redis logger *zap.Logger } func NewFeedRepo(redis *Redis, logger *zap.Logger) *FeedRepo { return &FeedRepo{redis: redis, logger: logger} } // Enabled reports whether the Redis-backed seen-set is available. func (r *FeedRepo) Enabled() bool { return r != nil && r.redis != nil } func (r *FeedRepo) key(userID string) string { return "feed:seen:" + userID } // Seen returns the post ids already served to the user this session (empty // when Redis is unavailable or the session is fresh). func (r *FeedRepo) Seen(ctx context.Context, userID string) []string { if !r.Enabled() { return nil } ids, err := r.redis.Client.SMembers(ctx, r.key(userID)).Result() if err != nil { // Degrade rather than fail the feed — worst case is a possible repeat. r.logger.Warn("feed seen: read failed", zap.String("user", userID), zap.Error(err)) return nil } return ids } // MarkSeen records the ids just served and refreshes the session TTL. func (r *FeedRepo) MarkSeen(ctx context.Context, userID string, ids []string) { if !r.Enabled() || len(ids) == 0 { return } members := make([]any, len(ids)) for i, id := range ids { members[i] = id } pipe := r.redis.Client.Pipeline() pipe.SAdd(ctx, r.key(userID), members...) pipe.Expire(ctx, r.key(userID), feedSeenTTL) if _, err := pipe.Exec(ctx); err != nil { r.logger.Warn("feed seen: write failed", zap.String("user", userID), zap.Error(err)) } } // Reset clears the user's seen-set — called when a fresh feed session begins // (the first page request) so reopening the feed serves from the top again. func (r *FeedRepo) Reset(ctx context.Context, userID string) { if !r.Enabled() { return } if err := r.redis.Client.Del(ctx, r.key(userID)).Err(); err != nil { r.logger.Warn("feed seen: reset failed", zap.String("user", userID), zap.Error(err)) } }