discord-bot/libs/DPP-10.0.29/include/dpp/event_router.h

768 lines
25 KiB
C++
Raw Permalink Normal View History

2024-02-22 16:09:56 -05:00
/************************************************************************************
*
* D++, A Lightweight C++ library for Discord
*
* Copyright 2021 Craig Edwards and D++ contributors
* (https://github.com/brainboxdotcc/DPP/graphs/contributors)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
************************************************************************************/
#pragma once
#include <dpp/export.h>
#include <string>
#include <map>
#include <variant>
#include <dpp/snowflake.h>
#include <dpp/misc-enum.h>
#include <dpp/json_fwd.h>
#include <algorithm>
#include <mutex>
#include <shared_mutex>
#include <cstring>
#include <atomic>
#include <dpp/exception.h>
#include <dpp/coro/job.h>
#include <dpp/coro/task.h>
namespace dpp {
#ifdef DPP_CORO
template <typename T>
class event_router_t;
namespace detail {
/** @brief Internal cogwheels for dpp::event_router_t */
namespace event_router {
/** @brief State of an owner of an event_router::awaitable */
enum class awaiter_state {
/** @brief Awaitable is not being awaited */
none,
/** @brief Awaitable is being awaited */
waiting,
/** @brief Awaitable will be resumed imminently */
resuming,
/** @brief Awaitable will be cancelled imminently */
cancelling
};
/**
* @brief Awaitable object representing an event.
* A user can co_await on this object to resume the next time the event is fired,
* optionally with a condition.
*/
template <typename T>
class awaitable {
friend class event_router_t<T>;
/** @brief Resume the coroutine waiting on this object */
void resume() {
std_coroutine::coroutine_handle<>::from_address(handle).resume();
}
/** @brief Event router that will manage this object */
event_router_t<T> *self;
/** @brief Predicate on the event, or nullptr for always match */
std::function<bool (const T &)> predicate = nullptr;
/** @brief Event that triggered a resumption, to give to the resumer */
const T *event = nullptr;
/** @brief Coroutine handle, type-erased */
void* handle = nullptr;
/** @brief The state of the awaiting coroutine */
std::atomic<awaiter_state> state = awaiter_state::none;
/** Default constructor is accessible only to event_router_t */
awaitable() = default;
/** Normal constructor is accessible only to event_router_t */
template <typename F>
awaitable(event_router_t<T> *router, F&& fun) : self{router}, predicate{std::forward<F>(fun)} {}
public:
/** This object is not copyable. */
awaitable(const awaitable &) = delete;
/** Move constructor. */
awaitable(awaitable &&rhs) noexcept : self{rhs.self}, predicate{std::move(rhs.predicate)}, event{rhs.event}, handle{std::exchange(rhs.handle, nullptr)}, state{rhs.state.load(std::memory_order_relaxed)} {}
/** This object is not copyable. */
awaitable& operator=(const awaitable &) = delete;
/** Move assignment operator. */
awaitable& operator=(awaitable&& rhs) noexcept {
self = rhs.self;
predicate = std::move(rhs.predicate);
event = rhs.event;
handle = std::exchange(rhs.handle, nullptr);
state = rhs.state.load(std::memory_order_relaxed);
return *this;
}
/**
* @brief Request cancellation. This will detach this object from the event router and resume the awaiter, which will be thrown dpp::task_cancelled::exception.
*
* @throw ??? As this resumes the coroutine, it may throw any exceptions at the caller.
*/
void cancel();
/**
* @brief First function called by the standard library when awaiting this object. Returns true if we need to suspend.
*
* @retval false always.
*/
[[nodiscard]] constexpr bool await_ready() const noexcept;
/**
* @brief Second function called by the standard library when awaiting this object, after suspension.
* This will attach the object to its event router, to be resumed on the next event that satisfies the predicate.
*
* @return void never resume on call.
*/
void await_suspend(detail::std_coroutine::coroutine_handle<> caller);
/**
* @brief Third and final function called by the standard library, called when resuming the coroutine.
*
* @throw @ref task_cancelled_exception if cancel() has been called
* @return const T& __Reference__ to the event that matched
*/
[[maybe_unused]] const T& await_resume();
};
}
}
#endif
/**
* @brief A returned event handle for an event which was attached
*/
typedef size_t event_handle;
/**
* @brief Handles routing of an event to multiple listeners.
* Multiple listeners may attach to the event_router_t by means of @ref operator()(F&&) "operator()". Passing a
* lambda into @ref operator()(F&&) "operator()" attaches to the event.
*
* @details Dispatchers of the event may call the @ref call() method to cause all listeners
* to receive the event.
*
* The @ref empty() method will return true if there are no listeners attached
* to the event_router_t (this can be used to save time by not constructing objects that
* nobody will ever see).
*
* The @ref detach() method removes an existing listener from the event,
* using the event_handle ID returned by @ref operator()(F&&) "operator()".
*
* This class is used by the library to route all websocket events to listening code.
*
* Example:
*
* @code{cpp}
* // Declare an event that takes log_t as its parameter
* event_router_t<log_t> my_event;
*
* // Attach a listener to the event
* event_handle id = my_event([&](const log_t& cc) {
* std::cout << cc.message << "\n";
* });
*
* // Construct a log_t and call the event (listeners will receive the log_t object)
* log_t lt;
* lt.message = "foo";
* my_event.call(lt);
*
* // Detach from an event using the handle returned by operator()
* my_event.detach(id);
* @endcode
*
* @tparam T type of single parameter passed to event lambda derived from event_dispatch_t
*/
template<class T> class event_router_t {
private:
friend class cluster;
/**
* @brief Non-coro event handler type
*/
using regular_handler_t = std::function<void(const T&)>;
/**
* @brief Type that event handlers will be stored as with DPP_CORO off.
* This is the ABI DPP_CORO has to match.
*/
using event_handler_abi_t = std::variant<regular_handler_t, std::function<task_dummy(T)>>;
#ifdef DPP_CORO
friend class detail::event_router::awaitable<T>;
/** @brief dpp::task coro event handler */
using task_handler_t = std::function<dpp::task<void>(const T&)>;
/** @brief Type that event handlers are stored as */
using event_handler_t = std::variant<regular_handler_t, task_handler_t>;
DPP_CHECK_ABI_COMPAT(event_handler_t, event_handler_abi_t)
#else
/**
* @brief Type that event handlers are stored as
*/
using event_handler_t = event_handler_abi_t;
#endif
/**
* @brief Identifier for the next event handler, will be given to the user on attaching a handler
*/
event_handle next_handle = 1;
/**
* @brief Thread safety mutex
*/
mutable std::shared_mutex mutex;
/**
* @brief Container of event listeners keyed by handle,
* as handles are handed out sequentially they will always
* be called in they order they are bound to the event
* as std::map is an ordered container.
*/
std::map<event_handle, event_handler_t> dispatch_container;
#ifdef DPP_CORO
/**
* @brief Mutex for messing with coro_awaiters.
*/
mutable std::shared_mutex coro_mutex;
/**
* @brief Vector containing the awaitables currently being awaited on for this event router.
*/
mutable std::vector<detail::event_router::awaitable<T> *> coro_awaiters;
#else
/**
* @brief Dummy for ABI compatibility between DPP_CORO and not
*/
utility::dummy<std::shared_mutex> definitely_not_a_mutex;
/**
* @brief Dummy for ABI compatibility between DPP_CORO and not
*/
utility::dummy<std::vector<void*>> definitely_not_a_vector;
#endif
/**
* @brief A function to be called whenever the method is called, to check
* some condition that is required for this event to trigger correctly.
*/
std::function<void(const T&)> warning;
/**
* @brief Next handle to be given out by the event router
*/
protected:
/**
* @brief Set the warning callback object used to check that this
* event is capable of running properly
*
* @param warning_function A checking function to call
*/
void set_warning_callback(std::function<void(const T&)> warning_function) {
warning = warning_function;
}
/**
* @brief Handle an event. This function should only be used without coro enabled, otherwise use handle_coro.
*/
void handle(const T& event) const {
if (warning) {
warning(event);
}
std::shared_lock l(mutex);
for (const auto& [_, listener] : dispatch_container) {
if (!event.is_cancelled()) {
if (std::holds_alternative<regular_handler_t>(listener)) {
std::get<regular_handler_t>(listener)(event);
} else {
throw dpp::logic_exception("cannot handle a coroutine event handler with a library built without DPP_CORO");
}
}
};
}
#ifdef DPP_CORO
/**
* @brief Handle an event as a coroutine, ensuring the lifetime of the event object.
*/
dpp::job handle_coro(T event) const {
if (warning) {
warning(event);
}
resume_awaiters(event);
std::vector<dpp::task<void>> tasks;
{
std::shared_lock l(mutex);
for (const auto& [_, listener] : dispatch_container) {
if (!event.is_cancelled()) {
if (std::holds_alternative<task_handler_t>(listener)) {
tasks.push_back(std::get<task_handler_t>(listener)(event));
} else if (std::holds_alternative<regular_handler_t>(listener)) {
std::get<regular_handler_t>(listener)(event);
}
}
};
}
for (dpp::task<void>& t : tasks) {
co_await t; // keep the event object alive until all tasks finished
}
}
/**
* @brief Attach a suspended coroutine to this event router via detail::event_router::awaitable.
* It will be resumed and detached when an event satisfying its condition completes, or it is cancelled.
*
* This is for internal usage only, the user way to do this is to co_await it (which will call this when suspending)
* This guarantees that the coroutine is indeed suspended and thus can be resumed at any time
*
* @param awaiter Awaiter to attach
*/
void attach_awaiter(detail::event_router::awaitable<T> *awaiter) {
std::unique_lock lock{coro_mutex};
coro_awaiters.emplace_back(awaiter);
}
/**
* @brief Detach an awaiting coroutine handle from this event router.
* This is mostly called when a detail::event_router::awaitable is cancelled.
*
* @param handle Coroutine handle to find in the attached coroutines
*/
void detach_coro(void *handle) {
std::unique_lock lock{coro_mutex};
coro_awaiters.erase(std::remove_if(coro_awaiters.begin(), coro_awaiters.end(), [handle](detail::event_router::awaitable<T> const *awaiter) { return awaiter->handle == handle; }), coro_awaiters.end());
}
/**
* @brief Resume any awaiter whose predicate matches this event, or is null.
*
* @param event Event to compare and pass to accepting awaiters
*/
void resume_awaiters(const T& event) const {
std::vector<detail::event_router::awaitable<T>*> to_resume;
std::unique_lock lock{coro_mutex};
for (auto it = coro_awaiters.begin(); it != coro_awaiters.end();) {
detail::event_router::awaitable<T>* awaiter = *it;
if (awaiter->predicate && !awaiter->predicate(event)) {
++it;
} else {
using state_t = detail::event_router::awaiter_state;
/**
* If state == none (was never awaited), do nothing
* If state == waiting, prevent resumption, resume on our end
* If state == resuming || cancelling, ignore
*
* Technically only cancelling || waiting should be possible here
* We do this by trying to exchange "waiting" with "resuming". If that returns false, this is presumed to be "cancelling"
*/
state_t s = state_t::waiting;
if (awaiter->state.compare_exchange_strong(s, state_t::resuming)) {
to_resume.emplace_back(awaiter);
awaiter->event = &event;
it = coro_awaiters.erase(it);
} else {
++it;
}
}
}
lock.unlock();
for (detail::event_router::awaitable<T>* awaiter : to_resume)
awaiter->resume();
}
#endif
public:
/**
* @brief Construct a new event_router_t object.
*/
event_router_t() = default;
/**
* @brief Destructor. Will cancel any coroutine awaiting on events.
*
* @throw ! Cancelling a coroutine will throw a dpp::task_cancelled_exception to it.
* This will be caught in this destructor, however, make sure no other exceptions are thrown in the coroutine after that or it will terminate.
*/
~event_router_t() {
#ifdef DPP_CORO
while (!coro_awaiters.empty()) {
// cancel all awaiters. here we cannot do the usual loop as we'd need to lock coro_mutex, and cancel() locks and modifies coro_awaiters
try {
coro_awaiters.back()->cancel();
/*
* will resume coroutines and may throw ANY exception, including dpp::task_cancelled_exception cancel() throws at them.
* we catch that one. for the rest, good luck :)
* realistically the only way any other exception would pop up here is if someone catches dpp::task_cancelled_exception THEN throws another exception.
*/
} catch (const dpp::task_cancelled_exception &) {
// ok. likely we threw this one
}
}
#endif
}
/**
* @brief Call all attached listeners.
* Listeners may cancel, by calling the event.cancel method.
*
* @param event Class to pass as parameter to all listeners.
*/
void call(const T& event) const {
#ifdef DPP_CORO
handle_coro(event);
#else
handle(event);
#endif
};
/**
* @brief Call all attached listeners.
* Listeners may cancel, by calling the event.cancel method.
*
* @param event Class to pass as parameter to all listeners.
*/
void call(T&& event) const {
#ifdef DPP_CORO
handle_coro(std::move(event));
#else
handle(std::move(event));
#endif
};
#ifdef DPP_CORO
/**
* @brief Obtain an awaitable object that refers to an event with a certain condition.
* It can be co_await-ed to wait for the next event that satisfies this condition.
* On resumption the awaiter will be given __a reference__ to the event,
* saving it in a variable is recommended to avoid variable lifetime issues.
*
* @details Example: @code{cpp}
* dpp::job my_handler(dpp::slashcommand_t event) {
* co_await event.co_reply(dpp::message().add_component(dpp::component().add_component().set_label("click me!").set_id("test")));
*
* dpp::button_click_t b = co_await c->on_button_click.with([](const dpp::button_click_t &event){ return event.custom_id == "test"; });
*
* // do something on button click
* }
* @endcode
*
* This can be combined with dpp::when_any and other awaitables, for example dpp::cluster::co_sleep to create @ref expiring-buttons "expiring buttons".
*
* @warning On resumption the awaiter will be given <b>a reference</b> to the event.
* This means that variable may become dangling at the next co_await, be careful and save it in a variable
* if you need to.
* @param pred Predicate to check the event against. This should be a callable of the form `bool(const T&)`
* where T is the event type, returning true if the event is to match.
* @return awaitable An awaitable object that can be co_await-ed to await an event matching the condition.
*/
template <typename Predicate>
#ifndef _DOXYGEN_
requires utility::callable_returns<Predicate, bool, const T&>
#endif
auto when(Predicate&& pred)
#ifndef _DOXYGEN_
noexcept(noexcept(std::function<bool(const T&)>{std::declval<Predicate>()}))
#endif
{
return detail::event_router::awaitable<T>{this, std::forward<Predicate>(pred)};
}
/**
* @brief Obtain an awaitable object that refers to any event.
* It can be co_await-ed to wait for the next event.
*
* Example:
* @details Example: @code{cpp}
* dpp::job my_handler(dpp::slashcommand_t event) {
* co_await event.co_reply(dpp::message().add_component(dpp::component().add_component().set_label("click me!").set_id("test")));
*
* dpp::button_click_t b = co_await c->on_message_create;
*
* // do something on button click
* }
* @endcode
*
* This can be combined with dpp::when_any and other awaitables, for example dpp::cluster::co_sleep to create expiring buttons.
*
* @warning On resumption the awaiter will be given <b>a reference</b> to the event.
* This means that variable may become dangling at the next co_await, be careful and save it in a variable
* if you need to.
* @return awaitable An awaitable object that can be co_await-ed to await an event matching the condition.
*/
[[nodiscard]] auto operator co_await() noexcept {
return detail::event_router::awaitable<T>{this, nullptr};
}
#endif
/**
* @brief Returns true if the container of listeners is empty,
* i.e. there is nothing listening for this event right now.
*
* @retval true if there are no listeners
* @retval false if there are some listeners
*/
[[nodiscard]] bool empty() const {
#ifdef DPP_CORO
std::shared_lock lock{mutex};
std::shared_lock coro_lock{coro_mutex};
return dispatch_container.empty() && coro_awaiters.empty();
#else
std::shared_lock lock{mutex};
return dispatch_container.empty();
#endif
}
/**
* @brief Returns true if any listeners are attached.
*
* This is the boolean opposite of event_router_t::empty().
* @retval true if listeners are attached
* @retval false if no listeners are attached
*/
operator bool() const {
return !empty();
}
#ifdef _DOXYGEN_
/**
* @brief Attach a callable to the event, adding a listener.
* The callable should either be of the form `void(const T&)` or
* `dpp::task<void>(const T&)` (the latter requires DPP_CORO to be defined),
* where T is the event type for this event router.
*
* This has the exact same behavior as using \ref attach(F&&) "attach".
*
* @see attach
* @param fun Callable to attach to event
* @return event_handle An event handle unique to this event, used to
* detach the listener from the event later if necessary.
*/
template <typename F>
[[maybe_unused]] event_handle operator()(F&& fun);
/**
* @brief Attach a callable to the event, adding a listener.
* The callable should either be of the form `void(const T&)` or
* `dpp::task<void>(const T&)` (the latter requires DPP_CORO to be defined),
* where T is the event type for this event router.
*
* @param fun Callable to attach to event
* @return event_handle An event handle unique to this event, used to
* detach the listener from the event later if necessary.
*/
template <typename F>
[[maybe_unused]] event_handle attach(F&& fun);
#else /* not _DOXYGEN_ */
# ifdef DPP_CORO
/**
* @brief Attach a callable to the event, adding a listener.
* The callable should either be of the form `void(const T&)` or
* `dpp::task<void>(const T&)`, where T is the event type for this event router.
*
* @param fun Callable to attach to event
* @return event_handle An event handle unique to this event, used to
* detach the listener from the event later if necessary.
*/
template <typename F>
requires (utility::callable_returns<F, dpp::job, const T&> || utility::callable_returns<F, dpp::task<void>, const T&> || utility::callable_returns<F, void, const T&>)
[[maybe_unused]] event_handle operator()(F&& fun) {
return this->attach(std::forward<F>(fun));
}
/**
* @brief Attach a callable to the event, adding a listener.
* The callable should either be of the form `void(const T&)` or
* `dpp::task<void>(const T&)`, where T is the event type for this event router.
*
* @param fun Callable to attach to event
* @return event_handle An event handle unique to this event, used to
* detach the listener from the event later if necessary.
*/
template <typename F>
requires (utility::callable_returns<F, void, const T&>)
[[maybe_unused]] event_handle attach(F&& fun) {
std::unique_lock l(mutex);
event_handle h = next_handle++;
dispatch_container.emplace(std::piecewise_construct, std::forward_as_tuple(h), std::forward_as_tuple(std::in_place_type_t<regular_handler_t>{}, std::forward<F>(fun)));
return h;
}
/**
* @brief Attach a callable to the event, adding a listener.
* The callable should either be of the form `void(const T&)` or
* `dpp::task<void>(const T&)`, where T is the event type for this event router.
*
* @param fun Callable to attach to event
* @return event_handle An event handle unique to this event, used to
* detach the listener from the event later if necessary.
*/
template <typename F>
requires (utility::callable_returns<F, task<void>, const T&>)
[[maybe_unused]] event_handle attach(F&& fun) {
assert(dpp::utility::is_coro_enabled());
std::unique_lock l(mutex);
event_handle h = next_handle++;
dispatch_container.emplace(std::piecewise_construct, std::forward_as_tuple(h), std::forward_as_tuple(std::in_place_type_t<task_handler_t>{}, std::forward<F>(fun)));
return h;
}
/**
* @brief Attach a callable to the event, adding a listener.
* The callable should either be of the form `void(const T&)` or
* `dpp::task<void>(const T&)`, where T is the event type for this event router.
*
* @deprecated dpp::job event handlers are deprecated and will be removed in a future version, use dpp::task<void> instead.
* @param fun Callable to attach to event
* @return event_handle An event handle unique to this event, used to
* detach the listener from the event later if necessary.
*/
template <typename F>
requires (utility::callable_returns<F, dpp::job, const T&>)
DPP_DEPRECATED("dpp::job event handlers are deprecated and will be removed in a future version, use dpp::task<void> instead")
[[maybe_unused]] event_handle attach(F&& fun) {
assert(dpp::utility::is_coro_enabled());
std::unique_lock l(mutex);
event_handle h = next_handle++;
dispatch_container.emplace(std::piecewise_construct, std::forward_as_tuple(h), std::forward_as_tuple(std::in_place_type_t<regular_handler_t>{}, std::forward<F>(fun)));
return h;
}
# else
/**
* @brief Attach a callable to the event, adding a listener.
* The callable should be of the form `void(const T&)`
* where T is the event type for this event router.
*
* @param fun Callable to attach to event
* @return event_handle An event handle unique to this event, used to
* detach the listener from the event later if necessary.
*/
template <typename F>
[[maybe_unused]] std::enable_if_t<utility::callable_returns_v<F, void, const T&>, event_handle> operator()(F&& fun) {
return this->attach(std::forward<F>(fun));
}
/**
* @brief Attach a callable to the event, adding a listener.
* The callable should be of the form `void(const T&)`
* where T is the event type for this event router.
*
* @warning You cannot call this within an event handler.
*
* @param fun Callable to attach to event
* @return event_handle An event handle unique to this event, used to
* detach the listener from the event later if necessary.
*/
template <typename F>
[[maybe_unused]] std::enable_if_t<utility::callable_returns_v<F, void, const T&>, event_handle> attach(F&& fun) {
std::unique_lock l(mutex);
event_handle h = next_handle++;
dispatch_container.emplace(h, std::forward<F>(fun));
return h;
}
# endif /* DPP_CORO */
#endif /* _DOXYGEN_ */
/**
* @brief Detach a listener from the event using a previously obtained ID.
*
* @warning You cannot call this within an event handler.
*
* @param handle An ID obtained from @ref operator(F&&) "operator()"
* @retval true The event was successfully detached
* @retval false The ID is invalid (possibly already detached, or does not exist)
*/
[[maybe_unused]] bool detach(const event_handle& handle) {
std::unique_lock l(mutex);
return this->dispatch_container.erase(handle);
}
};
#ifdef DPP_CORO
namespace detail::event_router {
template <typename T>
void awaitable<T>::cancel() {
awaiter_state s = awaiter_state::waiting;
/**
* If state == none (was never awaited), do nothing
* If state == waiting, prevent resumption, resume on our end
* If state == resuming || cancelling, ignore
*/
if (state.compare_exchange_strong(s, awaiter_state::cancelling)) {
self->detach_coro(handle);
resume();
}
}
template <typename T>
constexpr bool awaitable<T>::await_ready() const noexcept {
return false;
}
template <typename T>
void awaitable<T>::await_suspend(detail::std_coroutine::coroutine_handle<> caller) {
state.store(awaiter_state::waiting);
handle = caller.address();
self->attach_awaiter(this);
}
template <typename T>
const T &awaitable<T>::await_resume() {
handle = nullptr;
predicate = nullptr;
if (state.exchange(awaiter_state::none, std::memory_order_relaxed) == awaiter_state::cancelling) {
throw dpp::task_cancelled_exception{"event_router::awaitable was cancelled"};
}
return *std::exchange(event, nullptr);
}
}
#endif
} // namespace dpp