77 lines
2.5 KiB
Go
77 lines
2.5 KiB
Go
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))
|
|
}
|
|
}
|