6.2 KiB
Following System
Overview
System supports following different entity types:
- Authors
- Topics
- Communities
- Shouts (Posts)
GraphQL API
Mutations
follow
Follow an entity (author/topic/community/shout).
Parameters:
what: String!
- Entity type (AUTHOR
,TOPIC
,COMMUNITY
,SHOUT
)slug: String
- Entity slugentity_id: Int
- Optional entity ID
Returns:
{
authors?: Author[] // For AUTHOR type
topics?: Topic[] // For TOPIC type
communities?: Community[] // For COMMUNITY type
shouts?: Shout[] // For SHOUT type
error?: String // Error message if any
}
unfollow
Unfollow an entity.
Parameters: Same as follow
Returns: Same as follow
Important: Always returns current following list even if the subscription was not found, ensuring UI consistency.
Queries
get_shout_followers
Get list of authors who reacted to a shout.
Parameters:
slug: String
- Shout slugshout_id: Int
- Optional shout ID
Returns:
Author[] // List of authors who reacted
Caching System
Supported Entity Types
- Authors:
cache_author
,get_cached_follower_authors
- Topics:
cache_topic
,get_cached_follower_topics
- Communities: No cache
- Shouts: No cache
Cache Flow
- On follow/unfollow:
- Update entity in cache
- Invalidate user's following list cache (NEW)
- Update follower's following list
- Cache is updated before notifications
Cache Invalidation (NEW)
Following cache keys are invalidated after operations:
author:follows-topics:{user_id}
- After topic follow/unfollowauthor:follows-authors:{user_id}
- After author follow/unfollow
This ensures fresh data is fetched from database on next request.
Error Handling
Enhanced Error Handling (UPDATED)
- Unauthorized access check
- Entity existence validation
- Duplicate follow prevention
- Graceful handling of "following not found" errors
- Always returns current following list, even on errors
- Full error logging
- Transaction safety with
local_session()
Error Response Format
{
error?: "following was not found" | "invalid unfollow type" | "access denied",
topics?: Topic[], // Always present for topic operations
authors?: Author[], // Always present for author operations
// ... other entity types
}
Recent Fixes (NEW)
Issue 1: Stale UI State on Unfollow Errors
Problem: When unfollow operation failed with "following was not found", the client didn't update its state because it only processed successful responses.
Root Cause:
unfollow
mutation returned error with empty follows list[]
- Client logic:
if (result && !result.error)
prevented state updates on errors - User remained "subscribed" in UI despite no actual subscription in database
Solution:
- Always fetch current following list from cache/database
- Return actual following state even when subscription not found
- Add cache invalidation after successful operations
- Enhanced logging for debugging
Issue 2: Inconsistent Behavior in Follow Operations (NEW)
Problem: The follow
function had similar issues to unfollow
:
- Could return
None
instead of actual following list in error scenarios - Cache was not invalidated when trying to follow already-followed entities
- Inconsistent error handling between follow/unfollow operations
Root Cause:
follow
mutation could return{topics: null}
whenget_cached_follows_method
was not available- When user was already following an entity, cache invalidation was skipped
- Error responses didn't include current following state
Solution:
- Always return actual following list from cache/database
- Invalidate cache on every operation (both new and existing subscriptions)
- Add "already following" error while still returning current state
- Unified error handling consistent with unfollow
Code Changes
# UNFOLLOW - Before (BROKEN)
if sub:
# ... process unfollow
else:
return {"error": "following was not found", f"{entity_type}s": follows} # follows was []
# UNFOLLOW - After (FIXED)
if sub:
# ... process unfollow
# Invalidate cache
await redis.execute("DEL", f"author:follows-{entity_type}s:{follower_id}")
else:
error = "following was not found"
# Always get current state
existing_follows = await get_cached_follows_method(follower_id)
return {f"{entity_type}s": existing_follows, "error": error}
# FOLLOW - Before (BROKEN)
if existing_sub:
logger.info(f"User already following...")
# Cache not invalidated, could return stale data
else:
# ... create subscription
# Cache invalidated only here
follows = None # Could be None!
# ... complex logic to build follows list
return {f"{entity_type}s": follows} # follows could be None
# FOLLOW - After (FIXED)
if existing_sub:
error = "already following"
else:
# ... create subscription
# Always invalidate cache and get current state
await redis.execute("DEL", f"author:follows-{entity_type}s:{follower_id}")
existing_follows = await get_cached_follows_method(follower_id)
return {f"{entity_type}s": existing_follows, "error": error}
Impact
Before fixes:
- UI could show incorrect subscription state
- Cache inconsistencies between follow/unfollow operations
- Client-side logic
if (result && !result.error)
failed on valid error states
After fixes:
- ✅ UI always receives current subscription state
- ✅ Consistent cache invalidation on all operations
- ✅ Unified error handling between follow/unfollow
- ✅ Client can safely update UI even on error responses
Notifications
- Sent when author is followed/unfollowed
- Contains:
- Follower info
- Author ID
- Action type ("follow"/"unfollow")
Database Schema
Follower Tables
AuthorFollower
TopicFollower
CommunityFollower
ShoutReactionsFollower
Each table contains:
follower
- ID of following user{entity_type}
- ID of followed entity
Testing
Run the test script to verify fixes:
python test_unfollow_fix.py
Test Coverage
- ✅ Unfollow existing subscription
- ✅ Unfollow non-existent subscription
- ✅ Cache invalidation
- ✅ Proper error handling
- ✅ UI state consistency