-
Notifications
You must be signed in to change notification settings - Fork 201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add logic to load focused group members #6756
base: main
Are you sure you want to change the base?
Conversation
fe38a72
to
1054a69
Compare
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #6756 +/- ##
=======================================
Coverage 99.43% 99.43%
=======================================
Files 270 270
Lines 10235 10265 +30
Branches 2447 2451 +4
=======================================
+ Hits 10177 10207 +30
Misses 58 58 ☔ View full report in Codecov by Sentry. |
fc1e41b
to
4bac1dc
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took an initial pass over this. A few notes:
- I think we should defer fetching group members until either the point they are needed or shortly before (eg. when beginning to edit). That way we avoid these requests in sessions where no annotation editing happens. I think we could assume that any time an annotation is edited, the user may have need for at-mentions.
- Open groups can have members in the data model. We just haven't finished the work within h to enable non-admin users to invite them yet. Even though membership isn't needed to write in these groups, they will still be able to have members who are moderators. In general I think it would be a good idea to minimize the amount of special-casing we do for different group types, as this is liable to get out of date as Hypothesis group functionality evolves.
- Make sure we handle the case where the focused group is changed while group members are being fetched
src/sidebar/services/groups.ts
Outdated
this._store.isFeatureEnabled('at_mentions') && | ||
this._store.getFocusedGroupMembers() === null | ||
) { | ||
this._loadFocusedGroupMembers(groupId).catch(e => console.error(e)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This starts an async process to fetch focused group members, but this request is not canceled if the focused group is changed while it is in-flight. In general, code for dealing with async requests should assume:
- Requests can take an arbitrary amount of time (up to any explicit timeout)
- Requests may not finish in the order they are started
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, good point.
} | ||
|
||
private async _fetchAllMembers(groupId: string): Promise<GroupMember[]> { | ||
// Fetch first page of members, to determine how many more pages there are |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a race condition here that if group members are added while we are paging through, we will end up with duplicates. Unlikely, but possible in principle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True. Something similar could happen if a member that's already fetched in a previous page is removed. That could cause a member from a subsequent page to not be fetched since it has moved a position "up" and is now part of a page which is supposedly fetched already.
None of these issues are easy to handle with current API contract though, where pagination is not done via cursor, but good to have in mind.
@@ -18,12 +18,16 @@ export type State = { | |||
groups: Group[]; | |||
/** ID of currently selected group. */ | |||
focusedGroupId: string | null; | |||
|
|||
/** Members of currently selected group */ | |||
focusedGroupMembers: GroupMember[] | null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since only members for the current group are stored, this means we'll be reloading members each time you switch back and forth. That's OK to start with, as long as we ensure that this is cleared and in-flight requests are canceled when switching groups, but in future we might want to cache members from groups you've selected in the current session.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer not to do client-side caching for now, as it could lead to things changing in the server and having to find a way to invalidate the cache remotely, or risk having outdated lists of users until you reload.
For annotations we do not do caching, for example. Every time the group changes we fetch all annotations for that group. Although it's true that group members will usually change less frequently than annotations, and annotations are loaded in one go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fine, we can skip caching for an initial implementation.
4bac1dc
to
3616880
Compare
Thanks! I'll do the changes. |
92e9994
to
8447360
Compare
if (mentionsEnabled && focusedGroupMembers === null) { | ||
groupsService.loadFocusedGroupMembers(); | ||
} | ||
}, [focusedGroupMembers, groupsService, mentionsEnabled]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer not to add this as a side effect, but this component is loaded both when creating a new annotation or editing an existing one, so it's convenient to centralize the logic here.
const groupId = this._store.focusedGroupId(); | ||
if (!groupId) { | ||
return; // TODO Throw error? | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The store's loadFocusedGroupMembers
method already checks that a group is focused before its members are set, and throws an error otherwise, so I thought maybe we don't need to throw a similar error in two different places.
On the other hand, if one should silently skip, maybe it should be the store and not the service, and an error should be thrown here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the choice comes down to whether it is more useful for consumers if you allow them to call this when there might be no focused group, or whether such an action probably indicates an error on the caller's part that needs to be reported. Currently this should only be called in a context where there is a focused group.
At the moment it isn't clear to me that one approach is significantly better than the other.
const groupId = this._store.focusedGroupId(); | ||
if (!groupId) { | ||
return; // TODO Throw error? | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the choice comes down to whether it is more useful for consumers if you allow them to call this when there might be no focused group, or whether such an action probably indicates an error on the caller's part that needs to be reported. Currently this should only be called in a context where there is a focused group.
At the moment it isn't clear to me that one approach is significantly better than the other.
} | ||
|
||
const members = await this._fetchAllMembers(groupId); | ||
this._store.loadFocusedGroupMembers(members); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code still has the issue that the focused group could change in between the call to loadFocusedGroupMembers
and store.loadFocusedGroupMembers
being called. At a minimum you need to ensure that members loaded for group A don't get saved as the members of group B if the focused group changes from A to B while fetching. Ideally you also want to stop fetching any further pages of members for group A.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, sorry. This is not ready for review yet. I haven't finished applying all the changes from your previous review, but I forgot to move it to draft.
8447360
to
badea8d
Compare
acc2eb8
to
ebf1b56
Compare
ebf1b56
to
bca8af3
Compare
Implement logic to call members API for focused group, and load all its members in the store.
The loaded members are not used yet. They will be used in a follow-up PR to improve the at-mentions suggestions when focused group is not a public group.
Members loading is deferred until an annotation is created/edited for the first time after a group is focused, assuming they are not needed before that.
In future, if we decide to use group members for some different purpose, we may need to move the loading to a different point in the user journey.
Considerations
The group members API is paginated, so we load members in batches. This has two implications:
I'll try to check some real numbers of groups in production, to adjust this based on that.
We could probably parallelize these calls, loading 3 or 4 pages at a time. I'll explore this option separately, once I have the numbers.
Test steps
at_mentions
feature in http://localhost:5000/admin/featuresTODO