nostrum
nostrum copied to clipboard
Proposal: inline event awaiting
There's a pattern in some Discord libraries, when their language uses async-await syntax for concurrency, where they await on another event of a specific shape/pattern. From a BEAM perspective, this is suboptimal because it prevents the process from handling another message. That said, the ergonomics of writing the follow up to the awaited event can be captured with a relatively simple macro and a dedicated, opt-in consumer.
I propose the following macro syntax (macro name suggestions welcome):
nostrum_await event_pattern, time do
# make use of event_pattern here
else
# same as with's else
:timeout -> {} # if `time` (optional, default 5s) runs out
end
This will become a Task that first registers itself with the aforementioned consumer, then waits for the same pattern. The consumer will deregister the pattern after there is a match.
It should be safe to assume there will only be one await match from a given event, as any practical use case should be that specific.
Hmmmmmm. I wonder if we could implement this directly with BEAM's message passing constructs instead of custom macro magic? That way, it's also easier for something like a GenServer to subscribe to events.
Basically, we add ourselves into a list of event subscribers, and are sent the consumer events as messages. We can then do something like:
:ok = Nostrum.EventDispatch.subscribe()
receive do
{:MESSAGE_CREATE, %Message{content: ".decline"}} ->
Api.create_message!(channel, "Oh, fair enough.")
{:MESSAGE_CREATE, %Message{content: ".accept"}} ->
Api.create_message!(channel, "Congratulations! You are now enrolled to the nostrum newsletter.")
after
5_000 ->
Api.create_message!(channel, "No response received in time.")
end
:ok = Nostrum.EventDispatch.unsubscribe()
The EventDispatch
, or Registry
, or whatever we would use (I think Elixir's built-in Registry
already does this) could monitor processes and automatically unsubscribe them when they are done.
We might also want to namespace these events somehow: e.g. sending a :nostrum_event
as the first tuple element.
Thinking about this again, there's a contentious issue here between unblocking the consumer and being able to update the state based on the awaited event. My design enabled the former, but not the latter.
While the ergonomics was my primary goal, we could still make this simplified if we make an EphemeralConsumer. We can leave whether it blocks the originating consumer or not up to the user.
Maybe I'm misunderstanding the goal of this, but Nostrum currently uses a ConsumerSupervisor
for its event consumer, so every event received is already handled in a new process?
Yes, Nostrum currently uses a ConsumerSupervisor
so every event is handled in its own process.
As @khionu said, I think the biggest issue here is trying to maintain some semblance of state
between initial message and the follow up. I personally haven't used functionality like this before, but that seems important. Please correct me if I'm wrong!
There's also the additional complexity of where should these even listeners live? Are they defined in our current consumer handlers? I think that would make the most sense. Does this impose any design constraints?
In the past when people mentioned something like this, I always recommended rolling their own because it puts the onus of solving these issues on them :laughing:. But you guys are right, I do think we should offer some functionality like this.