220 lines
6.2 KiB
Markdown
220 lines
6.2 KiB
Markdown
# 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:**
|
|
```typescript
|
|
{
|
|
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 slug
|
|
- `shout_id: Int` - Optional shout ID
|
|
|
|
**Returns:**
|
|
```typescript
|
|
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
|
|
```typescript
|
|
{
|
|
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
|
|
```python
|
|
# 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:
|
|
```bash
|
|
python test_unfollow_fix.py
|
|
```
|
|
|
|
### Test Coverage
|
|
- ✅ Unfollow existing subscription
|
|
- ✅ Unfollow non-existent subscription
|
|
- ✅ Cache invalidation
|
|
- ✅ Proper error handling
|
|
- ✅ UI state consistency
|