feat: invalidate top-picks on watchlist changes
This commit is contained in:
@@ -14,22 +14,27 @@ import (
|
|||||||
type watchlistService struct {
|
type watchlistService struct {
|
||||||
repo domain.WatchlistRepository
|
repo domain.WatchlistRepository
|
||||||
jikan *jikan.Client
|
jikan *jikan.Client
|
||||||
|
invalidator domain.RecommendationInvalidator
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWatchlistService(repo domain.WatchlistRepository, jikan *jikan.Client) domain.WatchlistService {
|
func NewWatchlistService(repo domain.WatchlistRepository, jikan *jikan.Client, invalidator domain.RecommendationInvalidator) domain.WatchlistService {
|
||||||
return &watchlistService{repo: repo, jikan: jikan}
|
return &watchlistService{repo: repo, jikan: jikan, invalidator: invalidator}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *watchlistService) UpdateEntry(ctx context.Context, userID string, animeID int64, status string) error {
|
func (s *watchlistService) UpdateEntry(ctx context.Context, userID string, animeID int64, status string) error {
|
||||||
anime, fetchErr := s.jikan.GetAnimeByID(ctx, int(animeID))
|
var anime jikan.Anime
|
||||||
|
var fetchErr error
|
||||||
|
if s.jikan != nil {
|
||||||
|
anime, fetchErr = s.jikan.GetAnimeByID(ctx, int(animeID))
|
||||||
|
}
|
||||||
if fetchErr != nil {
|
if fetchErr != nil {
|
||||||
// still allow status updates for already-known anime rows
|
// still allow status updates for already-known anime rows
|
||||||
anime = jikan.Anime{}
|
anime = jikan.Anime{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.repo.InTx(ctx, func(txCtx context.Context, repo domain.WatchlistRepository) error {
|
if err := s.repo.InTx(ctx, func(txCtx context.Context, repo domain.WatchlistRepository) error {
|
||||||
_, err := repo.GetAnime(txCtx, animeID)
|
_, err := repo.GetAnime(txCtx, animeID)
|
||||||
if err != nil && fetchErr == nil {
|
if err != nil && fetchErr == nil && anime.MalID > 0 {
|
||||||
durationSeconds := anime.DurationSeconds()
|
durationSeconds := anime.DurationSeconds()
|
||||||
duration := sql.NullFloat64{Valid: durationSeconds > 0}
|
duration := sql.NullFloat64{Valid: durationSeconds > 0}
|
||||||
if duration.Valid {
|
if duration.Valid {
|
||||||
@@ -65,14 +70,28 @@ func (s *watchlistService) UpdateEntry(ctx context.Context, userID string, anime
|
|||||||
CurrentTimeSeconds: existing.CurrentTimeSeconds,
|
CurrentTimeSeconds: existing.CurrentTimeSeconds,
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
})
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.invalidateTopPicks(userID)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *watchlistService) RemoveEntry(ctx context.Context, userID string, animeID int64) error {
|
func (s *watchlistService) RemoveEntry(ctx context.Context, userID string, animeID int64) error {
|
||||||
return s.repo.DeleteWatchListEntry(ctx, db.DeleteWatchListEntryParams{
|
if err := s.repo.DeleteWatchListEntry(ctx, db.DeleteWatchListEntryParams{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
AnimeID: animeID,
|
AnimeID: animeID,
|
||||||
})
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.invalidateTopPicks(userID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *watchlistService) invalidateTopPicks(userID string) {
|
||||||
|
if s.invalidator != nil {
|
||||||
|
s.invalidator.InvalidateTopPicksForUser(userID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *watchlistService) GetWatchlist(ctx context.Context, userID string) ([]domain.UserWatchListRow, error) {
|
func (s *watchlistService) GetWatchlist(ctx context.Context, userID string) ([]domain.UserWatchListRow, error) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
func TestWatchlistServiceGetWatchlistMap(t *testing.T) {
|
func TestWatchlistServiceGetWatchlistMap(t *testing.T) {
|
||||||
repo := &fakeWatchlistRepository{watchlistAnimeIDs: []int64{1, 3}}
|
repo := &fakeWatchlistRepository{watchlistAnimeIDs: []int64{1, 3}}
|
||||||
svc := NewWatchlistService(repo, nil)
|
svc := NewWatchlistService(repo, nil, nil)
|
||||||
|
|
||||||
got, err := svc.GetWatchlistMap(context.Background(), "user-1", []int64{1, 2, 3})
|
got, err := svc.GetWatchlistMap(context.Background(), "user-1", []int64{1, 2, 3})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -28,7 +28,7 @@ func TestWatchlistServiceGetWatchlistMap(t *testing.T) {
|
|||||||
|
|
||||||
func TestWatchlistServiceGetWatchlistMapSkipsEmptyInputs(t *testing.T) {
|
func TestWatchlistServiceGetWatchlistMapSkipsEmptyInputs(t *testing.T) {
|
||||||
repo := &fakeWatchlistRepository{}
|
repo := &fakeWatchlistRepository{}
|
||||||
svc := NewWatchlistService(repo, nil)
|
svc := NewWatchlistService(repo, nil, nil)
|
||||||
|
|
||||||
got, err := svc.GetWatchlistMap(context.Background(), "", []int64{1})
|
got, err := svc.GetWatchlistMap(context.Background(), "", []int64{1})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -52,7 +52,7 @@ func TestWatchlistServiceGetWatchlistMapSkipsEmptyInputs(t *testing.T) {
|
|||||||
|
|
||||||
func TestWatchlistServiceDeleteContinueWatchingClearsProgressInTransaction(t *testing.T) {
|
func TestWatchlistServiceDeleteContinueWatchingClearsProgressInTransaction(t *testing.T) {
|
||||||
repo := &fakeWatchlistRepository{}
|
repo := &fakeWatchlistRepository{}
|
||||||
svc := NewWatchlistService(repo, nil)
|
svc := NewWatchlistService(repo, nil, nil)
|
||||||
|
|
||||||
if err := svc.DeleteContinueWatching(context.Background(), "user-1", 12); err != nil {
|
if err := svc.DeleteContinueWatching(context.Background(), "user-1", 12); err != nil {
|
||||||
t.Fatalf("DeleteContinueWatching: %v", err)
|
t.Fatalf("DeleteContinueWatching: %v", err)
|
||||||
@@ -76,7 +76,7 @@ func TestWatchlistServiceDeleteContinueWatchingClearsProgressInTransaction(t *te
|
|||||||
|
|
||||||
func TestWatchlistServiceDeleteContinueWatchingStopsAfterDeleteError(t *testing.T) {
|
func TestWatchlistServiceDeleteContinueWatchingStopsAfterDeleteError(t *testing.T) {
|
||||||
repo := &fakeWatchlistRepository{deleteContinueErr: errors.New("delete failed")}
|
repo := &fakeWatchlistRepository{deleteContinueErr: errors.New("delete failed")}
|
||||||
svc := NewWatchlistService(repo, nil)
|
svc := NewWatchlistService(repo, nil, nil)
|
||||||
|
|
||||||
if err := svc.DeleteContinueWatching(context.Background(), "user-1", 12); err == nil || err.Error() != "delete failed" {
|
if err := svc.DeleteContinueWatching(context.Background(), "user-1", 12); err == nil || err.Error() != "delete failed" {
|
||||||
t.Fatalf("DeleteContinueWatching error = %v, want delete failed", err)
|
t.Fatalf("DeleteContinueWatching error = %v, want delete failed", err)
|
||||||
@@ -88,7 +88,8 @@ func TestWatchlistServiceDeleteContinueWatchingStopsAfterDeleteError(t *testing.
|
|||||||
|
|
||||||
func TestWatchlistServiceRemoveEntry(t *testing.T) {
|
func TestWatchlistServiceRemoveEntry(t *testing.T) {
|
||||||
repo := &fakeWatchlistRepository{}
|
repo := &fakeWatchlistRepository{}
|
||||||
svc := NewWatchlistService(repo, nil)
|
invalidator := &fakeRecommendationInvalidator{}
|
||||||
|
svc := NewWatchlistService(repo, nil, invalidator)
|
||||||
|
|
||||||
if err := svc.RemoveEntry(context.Background(), "user-1", 9); err != nil {
|
if err := svc.RemoveEntry(context.Background(), "user-1", 9); err != nil {
|
||||||
t.Fatalf("RemoveEntry: %v", err)
|
t.Fatalf("RemoveEntry: %v", err)
|
||||||
@@ -96,6 +97,22 @@ func TestWatchlistServiceRemoveEntry(t *testing.T) {
|
|||||||
if repo.deletedWatchlist.UserID != "user-1" || repo.deletedWatchlist.AnimeID != 9 {
|
if repo.deletedWatchlist.UserID != "user-1" || repo.deletedWatchlist.AnimeID != 9 {
|
||||||
t.Fatalf("delete params = %#v", repo.deletedWatchlist)
|
t.Fatalf("delete params = %#v", repo.deletedWatchlist)
|
||||||
}
|
}
|
||||||
|
if invalidator.userID != "user-1" {
|
||||||
|
t.Fatalf("invalidated user = %q, want user-1", invalidator.userID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWatchlistServiceUpdateEntryInvalidatesRecommendations(t *testing.T) {
|
||||||
|
repo := &fakeWatchlistRepository{}
|
||||||
|
invalidator := &fakeRecommendationInvalidator{}
|
||||||
|
svc := NewWatchlistService(repo, nil, invalidator)
|
||||||
|
|
||||||
|
if err := svc.UpdateEntry(context.Background(), "user-1", 9, "watching"); err != nil {
|
||||||
|
t.Fatalf("UpdateEntry: %v", err)
|
||||||
|
}
|
||||||
|
if invalidator.userID != "user-1" {
|
||||||
|
t.Fatalf("invalidated user = %q, want user-1", invalidator.userID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeWatchlistRepository struct {
|
type fakeWatchlistRepository struct {
|
||||||
@@ -110,6 +127,14 @@ type fakeWatchlistRepository struct {
|
|||||||
deletedWatchlist db.DeleteWatchListEntryParams
|
deletedWatchlist db.DeleteWatchListEntryParams
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fakeRecommendationInvalidator struct {
|
||||||
|
userID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *fakeRecommendationInvalidator) InvalidateTopPicksForUser(userID string) {
|
||||||
|
i.userID = userID
|
||||||
|
}
|
||||||
|
|
||||||
func (r *fakeWatchlistRepository) InTx(ctx context.Context, fn func(context.Context, domain.WatchlistRepository) error) error {
|
func (r *fakeWatchlistRepository) InTx(ctx context.Context, fn func(context.Context, domain.WatchlistRepository) error) error {
|
||||||
r.inTxCalled = true
|
r.inTxCalled = true
|
||||||
return fn(ctx, r)
|
return fn(ctx, r)
|
||||||
|
|||||||
Reference in New Issue
Block a user