core/docs/follower.md

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 slug
  • entity_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 users who reacted to a shout.

Parameters:

  • slug: String - Shout slug
  • shout_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

  1. On follow/unfollow:
    • Update entity in cache
    • Invalidate user's following list cache (NEW)
    • Update follower's following list
  2. Cache is updated before notifications

Cache Invalidation (NEW)

Following cache keys are invalidated after operations:

  • author:follows-topics:{user_id} - After topic follow/unfollow
  • author: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:

  1. unfollow mutation returned error with empty follows list []
  2. Client logic: if (result && !result.error) prevented state updates on errors
  3. User remained "subscribed" in UI despite no actual subscription in database

Solution:

  1. Always fetch current following list from cache/database
  2. Return actual following state even when subscription not found
  3. Add cache invalidation after successful operations
  4. 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:

  1. follow mutation could return {topics: null} when get_cached_follows_method was not available
  2. When user was already following an entity, cache invalidation was skipped
  3. Error responses didn't include current following state

Solution:

  1. Always return actual following list from cache/database
  2. Invalidate cache on every operation (both new and existing subscriptions)
  3. Add "already following" error while still returning current state
  4. 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