From 57e7eef08296acf97d9c7a7b25d8a6d268ab52c3 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Thu, 10 Sep 2026 03:47:00 -0600 Subject: [PATCH] Fix StoryIndexService --- app/Observers/FollowerObserver.php | 4 +++ app/Services/StoryIndexService.php | 43 +++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/app/Observers/FollowerObserver.php b/app/Observers/FollowerObserver.php index b6f8a1320..d337fc41a 100644 --- a/app/Observers/FollowerObserver.php +++ b/app/Observers/FollowerObserver.php @@ -6,6 +6,7 @@ use App\Jobs\HomeFeedPipeline\FeedFollowPipeline; use App\Jobs\HomeFeedPipeline\FeedUnfollowPipeline; use App\Models\Follower; use App\Services\FollowerService; +use App\Services\StoryIndexService; use Illuminate\Support\Facades\Cache; class FollowerObserver @@ -22,6 +23,7 @@ class FollowerObserver } FollowerService::add($follower->profile_id, $follower->following_id); + app(StoryIndexService::class)->addFollowing($follower->profile_id, $follower->following_id); FeedFollowPipeline::dispatch($follower->profile_id, $follower->following_id)->onQueue('follow'); } @@ -33,6 +35,7 @@ class FollowerObserver public function deleted(Follower $follower) { FollowerService::remove($follower->profile_id, (string) $follower->following_id); + app(StoryIndexService::class)->removeFollowing($follower->profile_id, $follower->following_id); FeedUnfollowPipeline::dispatch($follower->profile_id, $follower->following_id)->onQueue('feed'); } @@ -44,5 +47,6 @@ class FollowerObserver public function forceDeleted(Follower $follower) { FollowerService::remove($follower->profile_id, (string) $follower->following_id); + app(StoryIndexService::class)->removeFollowing($follower->profile_id, $follower->following_id); } } diff --git a/app/Services/StoryIndexService.php b/app/Services/StoryIndexService.php index b35112c2c..85cdf5256 100644 --- a/app/Services/StoryIndexService.php +++ b/app/Services/StoryIndexService.php @@ -176,7 +176,16 @@ class StoryIndexService $path = $story->path; Redis::pipeline(function ($pipe) use ( - $author, $sid, $score, $ttl, $duration, $overlays, $viewCount, $createdIso, $type, $path + $author, + $sid, + $score, + $ttl, + $duration, + $overlays, + $viewCount, + $createdIso, + $type, + $path ) { $keyStory = $this->storyKey($sid); $keyAuth = $this->authorKey($author); @@ -235,6 +244,36 @@ class StoryIndexService Redis::expire($key, $finalTtl); } + /** + * Keep the cached following set in sync when a follow is created. + * Only touches the key if it already exists; otherwise the next carousel + * fetch hydrates from SQL and picks up the new follow anyway. + */ + public function addFollowing(int $followerId, int $followingId): void + { + $key = "following:{$followerId}"; + + if (! Redis::exists($key)) { + return; + } + + Redis::sadd($key, (string) $followingId); + } + + /** + * Keep the cached following set in sync when a follow is removed. + */ + public function removeFollowing(int $followerId, int $followingId): void + { + $key = "following:{$followerId}"; + + if (! Redis::exists($key)) { + return; + } + + Redis::srem($key, (string) $followingId); + } + public function rebuildIndex(): array { $lockKey = $this->rebuildLockKey(); @@ -331,7 +370,6 @@ class StoryIndexService 'message' => 'Story index and seen data rebuilt successfully', 'stats' => $stats, ]; - } finally { Redis::del($lockKey); } @@ -657,7 +695,6 @@ class StoryIndexService if (! $hasResults) { Redis::pipeline(function ($pipe) use ($followingKey) { $pipe->sadd($followingKey, '__empty__'); - $pipe->srem($followingKey, '__empty__'); $pipe->expire($followingKey, 3600); }); } else {