-
Notifications
You must be signed in to change notification settings - Fork 78
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
feat: polls rewrite #1373
Conversation
…e to vote_changed + vote_removed
…f multiple votes allowed
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; | ||
}; |
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.
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?
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.
(including subscribePollUpdated
)
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 a no-go, it was indeed our initial approach however there are a few issues with it:
- Having
3000
polls essentially means having3000 * 5
listeners at a certain point, all of which would be invoked when a new event arrives (and2999
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.
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.
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.
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.
If I understand you correctly, you're looking for something like:
- The
pollManager
is still listening forpoll
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 thepoll
manager, which would act as arouter
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 whenevermessages
actually arrive within the stateclient.queryChannels
orchannel.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 topoll.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) => { |
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.
public fromState = (id: string) => { | |
public fromCache = (id: string) => { |
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.
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
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'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.
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.
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 = {}) => { |
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.
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)
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.
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.
src/poll_manager.ts
Outdated
const { poll } = await this.client.getPoll(id); | ||
|
||
this.setOrOverwriteInCache(poll, true); | ||
|
||
return this.fromState(id); |
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.
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?
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.
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.
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 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?
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 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) ?
const { poll: createdPoll } = await this.client.createPoll(poll); | ||
|
||
return new Poll({ client: this.client, poll: createdPoll }); |
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.
Why is the newly created poll not being cached?
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.
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.
CLA
Description of the changes, What, Why and How?
Changelog