Skip to content
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

feat: polls rewrite #1373

Merged
merged 61 commits into from
Oct 30, 2024
Merged

feat: polls rewrite #1373

merged 61 commits into from
Oct 30, 2024

Conversation

MartinCupela
Copy link
Contributor

CLA

  • I have signed the Stream CLA (required).
  • Code changes are tested

Description of the changes, What, Why and How?

Changelog

MartinCupela and others added 30 commits September 18, 2024 11:05
Comment on lines +108 to +138
private subscribePollClosed = () => {
return this.client.on('poll.closed', (event) => {
if (event.poll?.id) {
this.fromState(event.poll.id)?.handlePollClosed(event);
}
}).unsubscribe;
};

private subscribeVoteCasted = () => {
return this.client.on('poll.vote_casted', (event) => {
if (event.poll?.id) {
this.fromState(event.poll.id)?.handleVoteCasted(event);
}
}).unsubscribe;
};

private subscribeVoteChanged = () => {
return this.client.on('poll.vote_changed', (event) => {
if (event.poll?.id) {
this.fromState(event.poll.id)?.handleVoteChanged(event);
}
}).unsubscribe;
};

private subscribeVoteRemoved = () => {
return this.client.on('poll.vote_removed', (event) => {
if (event.poll?.id) {
this.fromState(event.poll.id)?.handleVoteRemoved(event);
}
}).unsubscribe;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come these are not handled within Poll instance itself? If it's loaded, it handles its own events, if it's not then it's not really necessary to care for it - no?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(including subscribePollUpdated)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a no-go, it was indeed our initial approach however there are a few issues with it:

  • Having 3000 polls essentially means having 3000 * 5 listeners at a certain point, all of which would be invoked when a new event arrives (and 2999 of them would simply skip because the event does not concern them)
  • It doesn't particularly scale well with more polls being loaded through pagination
  • This way, we only have a single set of listeners that consume one event at a time, updating the correct poll from within the cache directly (while maintaining referential integrity of the poll instances at all times)

So, in other words - they are indeed handled in the Poll instances themselves it's just not the Poll instances that listen to the events but rather the manager does.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So esentially - poll instance cannot properly live without the manager - correct? You always have to rely on manager to keep your poll instance up to date so "detached" instances are "broken" in a way.

I think it'd be good if we explored the idea of targeted events deeper - it should be very simple to do for polls as most of the events are poll-related and carry message/poll id's and we could benefit from this performance enhancement in the future. Poll could be fully encapsulated with logic it should "own" allowing it also to live in a detached (from the manager) state.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand you correctly, you're looking for something like:

  • The pollManager is still listening for poll events
  • It re-dispatches a targeted event (for example, poll.vote_casted-<poll_id> or something`)
  • The poll itself is only listening to poll.vote_casted-<this.id> events and handles them

Is this correct ? I totally get your concerns and something like this was briefly considered too, however I do have some concerns about this approach too:

  • Each poll having its own listener can potentially be a ton of extra listeners
  • Not every listener would indeed be triggered (only the specific ones), but their event's dispatch would still be coupled with the poll manager, which would act as a router of sorts in order to orchestrate the traffic - doing these changes through events seems to me like a bit of an overhead considering that we can just update inline
  • If poll instances are indeed to listen, they have to be subscribed to events; since we need all of these to be up to date at all times we would basically need to subscribe to the new events once again whenever messages actually arrive within the state client.queryChannels or channel.query for example)

With that said, I do see some really good things with this as well:

  • The ability to hook up to specific poll events from anywhere (for example, if some custom integrator logic depends on events they can simply hook up to poll.vote_casted-<id> and not have to do an ID sanity check each time one of these events is fired)
  • Having them detached is a very, very good point too

So, I'm not particularly sure which way we should go - perhaps we should sit down and discuss this more thoroughly. Adding targeted events shouldn't be difficult as far as I can see (pretty much the same logic).

return this.pollCache;
}

public fromState = (id: string) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public fromState = (id: string) => {
public fromCache = (id: string) => {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberated quite a bit over this as well, but in the end I decided to go with fromState since the cache is the manager's state in some sense. It feels kind of wrong exposing something called fromCache to integrators since it might lead them to believe it's something else, but I might just be overthinking this :D

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried that this might be a bit misleading either way as integrators might think that manager has a state which they can subscribe to as the API becomes the norm across the client though I understand your point too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like byId would also be good ? Not implying anything specifically that way, we're just saying that we're asking for a poll with a certain ID. What do you think ?

return this.fromState(id);
};

public queryPolls = async (filter: QueryPollsFilters, sort: PollSort = [], options: QueryPollsOptions = {}) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StreamChat.queryThreads returns list of Thread instances - maybe we could do the same here for general API consistency?

The same goes for state hydration:

poll.hydrateState(differentPollInstanceButSameId)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, client.queryPolls should return the raw response in my opinion in case someone needs it (they'll have a way to do it). If they're reaching for reactive state though, they would fetch through pollManager.queryPolls(...) - this was the general idea. We can of course move the instantiation there too and simply make sure the cache is populated in pollManager, but it seemed like losing an API that way.

Comment on lines 58 to 62
const { poll } = await this.client.getPoll(id);

this.setOrOverwriteInCache(poll, true);

return this.fromState(id);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we reaching for the request first instead of cache - I'd expect we'd be reaching for request in case of cache-miss?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we've decided that queryPolls and getPoll are always going to reinitialize the state within the cache. We want them to be used independently from the rest and be able to overwrite whatever's in the cache (not by reference though, only state) when they're queried. But, perhaps we can simply always use getPoll and provide a fallback for whenever it's not in the cache I suppose. Wanted to keep those 2 separate though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There shouldn't be a need for it though, no? If the poll is in the cache then that means it's been receiving updates through event subscriptions meaning its state should closely represent the one that backend has - hydrating it at this point seems excessive.

The other way would be to return it right away and do the state hydration behind the scenes - since the state of the poll is reactive, it should not matter that it changes after it gets returned, no?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a particular need for this for offline storage in RN for example. Let us consider the following scenario:

  • You create a poll and are actively watching its progress (some votes, answers come in - w/e)
  • You close the app and do something else, while in the meantime more votes and/or answers arrive
  • You come back, and the first "optimistic" thing that loads is the data from the local DB (which is now out of date since those WS events never got consumed while you weren't using the app)
  • If you're offline, this is no biggie since we don't expect that WS events are handled in the background
  • If you're online however, the point at which a query is made for all of the channels (and their messages) you'll get all of the updated data; and it should indeed overwrite the local DB data (and store this agaiån)

Unfortunately, the sync API does not yet support poll events and so we can't particularly rely on it to do the heavy lifting for us. Whenever these potentially get added in the future, perhaps we can remove the overwriting mechanism (it's anyway hidden behind the second argument). For now, however - I believe we need it.

If it seems sketchy to you maybe we can lift the overwriting condition higher so that we can specifically trigger this only for the RN SDK (since the rest don't have offline support) ?

Comment on lines +52 to +54
const { poll: createdPoll } = await this.client.createPoll(poll);

return new Poll({ client: this.client, poll: createdPoll });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the newly created poll not being cached?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not since presence in the cache is directly tied to the poll actually being sent as a message somewhere (i.e appearing when we queryChannels, or a message.new event arrives with a poll inside of it etc). But it might be a good point to perhaps not even create a poll instance, since in this case we're really only interested in the ID of the poll that got created.

@isekovanic isekovanic merged commit 7c56d21 into master Oct 30, 2024
5 checks passed
@isekovanic isekovanic deleted the feat/polls-rewrite branch October 30, 2024 16:09
@github-actions github-actions bot mentioned this pull request Oct 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants