nostrum icon indicating copy to clipboard operation
nostrum copied to clipboard

Proposal: inline event awaiting

Open khionu opened this issue 3 years ago • 4 comments

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.

khionu avatar Sep 02 '21 02:09 khionu

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.

jchristgit avatar Sep 03 '21 17:09 jchristgit

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.

khionu avatar Sep 03 '21 19:09 khionu

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?

Th3-M4jor avatar Sep 03 '21 20:09 Th3-M4jor

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.

Kraigie avatar Sep 04 '21 16:09 Kraigie