-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Race Condition: ownReactions Reset When Adding Multiple Reactions
Description
When adding multiple reactions to an activity in quick succession, the ownReactions field gets reset to an empty or incomplete list after the second reaction is added. This prevents users from seeing their own reactions correctly in the UI.
Environment
- SDK Version:
stream_feeds: ^0.3.1 - Dart SDK: 3.9.0
Steps to Reproduce
- Connect to a feed and load activities
- Add a reaction to an activity using
feed.addActivityReaction() - Immediately add a second reaction (different type) to the same activity
- Observe that
ActivityData.ownReactionsis reset/incomplete after the second reaction
Code Example:
// First reaction - works correctly
await feed.addActivityReaction(
activityId: activity.id,
request: AddReactionRequest(
type: 'like',
createNotificationActivity: false,
enforceUnique: false,
),
);
// Second reaction - causes ownReactions to reset
await feed.addActivityReaction(
activityId: activity.id,
request: AddReactionRequest(
type: 'heart',
createNotificationActivity: false,
enforceUnique: false,
),
);
// After second reaction, activity.ownReactions is incomplete or emptyExpected Behavior
After adding multiple reactions, ActivityData.ownReactions should contain all reactions added by the current user:
// Expected after adding two reactions
activity.ownReactions.length == 2 // Contains both 'like' and 'heart'Actual Behavior
After adding the second reaction, ActivityData.ownReactions is reset to an empty list or contains only the most recent reaction:
// Actual behavior
activity.ownReactions.length == 0 // or == 1 (only latest reaction)Root Cause Analysis
The issue appears to be a race condition between WebSocket events in the state management layer:
- User adds reaction →
addActivityReaction()API call (no optimistic update) - WebSocket receives
ActivityReactionAddedEvent→onReactionAdded()correctly updatesownReactions✅ - Then WebSocket receives
ActivityUpdatedEvent(or similar) →onActivityUpdated()replaces the entire activity with potentially stale data ❌
The problem occurs in the state notifiers:
FeedStateNotifier.onActivityUpdated()(feed_state.dart:122-139)ActivityListStateNotifier.onActivityUpdated()(activity_list_state.dart:61-70)
Both methods use sortedUpsert() which replaces the entire ActivityData object, including user-specific fields like ownReactions and ownBookmarks that may be more up-to-date in the local state than in the WebSocket event payload.
Impact
- Users cannot see their own reactions after adding multiple reactions
- Similar issue likely affects
ownBookmarks - Impacts any user-specific activity fields that can change rapidly
- Creates poor UX as reactions appear to disappear
Proposed Solution
Modify onActivityUpdated() in both state notifiers to intelligently merge incoming updates with existing state, preserving user-specific fields that might be more current locally:
High-Level Approach
- Before updating, check if the activity already exists in state
- If it exists, compare user-specific fields (
ownReactions,ownBookmarks) between existing and incoming data - Preserve the version with more items (reasonable heuristic for "more up-to-date")
- Apply the merge before calling
sortedUpsert()
Example Implementation
void onActivityUpdated(ActivityData activity) {
// Find existing activity in state
final existingActivity = state.activities.firstWhereOrNull(
(it) => it.id == activity.id,
);
// Merge with existing to preserve user-specific fields
final mergedActivity = existingActivity != null
? activity.copyWith(
ownReactions: _mergeList(
existing: existingActivity.ownReactions,
incoming: activity.ownReactions,
),
ownBookmarks: _mergeList(
existing: existingActivity.ownBookmarks,
incoming: activity.ownBookmarks,
),
)
: activity;
final updatedActivities = state.activities.sortedUpsert(
mergedActivity,
key: (it) => it.id,
compare: activitiesSort.compare,
);
state = state.copyWith(activities: updatedActivities);
}
// Helper: Keep the list with more items (assumes it's more up-to-date)
List<T> _mergeList<T>({
required List<T> existing,
required List<T> incoming,
}) {
if (incoming.length > existing.length) return incoming;
if (existing.length > incoming.length) return existing;
return incoming; // Same length, prefer incoming
}Alternative Solutions
- Optimistic Updates: Update local state immediately when calling
addActivityReaction(), before WebSocket confirmation - Event Timestamp Comparison: Only apply updates if incoming event timestamp is newer than local state
- Debouncing: Debounce
onActivityUpdated()to reduce race condition window
Workaround
As a temporary workaround, you could implement optimistic updates in your application code:
// Manually update state before API call
final optimisticActivity = activity.addReaction(newReaction, currentUserId);
// Update your local state with optimisticActivity
await feed.addActivityReaction(...);However, this is not ideal as it requires application-level state management duplication.
Additional Context
This issue is particularly noticeable when:
- Users add multiple reactions quickly (e.g., like + heart)
- Network latency causes WebSocket events to arrive close together
- Working with high-frequency user interactions
Files Affected
packages/stream_feeds/lib/src/state/feed_state.dart(line 122-139)packages/stream_feeds/lib/src/state/activity_list_state.dart(line 61-70)- Event handlers in
packages/stream_feeds/lib/src/state/event/(indirect)
I'm happy to provide more details or test a fix if needed. Thank you for maintaining this excellent SDK!