#pragma once #include "Result.hpp" #include "MiniFunction.hpp" #include "../loader/Event.hpp" namespace geode { namespace impl { struct DefaultProgress { std::string message; std::optional percentage; DefaultProgress() = default; DefaultProgress(std::string const& msg) : message(msg) {} DefaultProgress(auto msg, uint8_t percentage) : message(msg), percentage(percentage) {} }; } struct CancelledState final {}; template class PromiseEventFilter; template class Promise final { public: using Value = T; using Error = E; using Progress = P; using OnResolved = utils::MiniFunction; using OnRejected = utils::MiniFunction; using OnProgress = utils::MiniFunction; using OnFinished = utils::MiniFunction; using OnCancelled = utils::MiniFunction; using State = std::variant; using OnStateChange = utils::MiniFunction; // These are needed if for example Value and Error are the same type static constexpr size_t STATE_VALUE_INDEX = 0; static constexpr size_t STATE_ERROR_INDEX = 1; static constexpr size_t STATE_PROGRESS_INDEX = 2; static constexpr size_t STATE_CANCELLED_INDEX = 3; Promise() : m_data(std::make_shared()) {} Promise(utils::MiniFunction source, bool threaded = true) : Promise([source](auto resolve, auto reject, auto, auto const&) { source(resolve, reject); }, threaded) {} Promise(utils::MiniFunction source, bool threaded = true) : Promise([source](auto onStateChanged, auto const& cancelled) { source( [onStateChanged](auto&& value) { onStateChanged(State(std::in_place_index, value)); }, [onStateChanged](auto&& error) { onStateChanged(State(std::in_place_index, error)); }, [onStateChanged](auto&& progress) { onStateChanged(State(std::in_place_index, progress)); }, cancelled ); }, threaded, std::monostate()) {} Promise(utils::MiniFunction source, bool threaded, std::monostate tag) : m_data(std::make_shared()) { m_data->shouldStartThreaded = threaded; if (threaded) { std::thread([source = std::move(source), data = m_data]() mutable { Promise::invoke_source(std::move(source), data); }).detach(); } else { Promise::invoke_source(std::move(source), m_data); } } Promise then(utils::MiniFunction&& callback) { return this->template then([callback](auto value) { callback(value); return std::move(value); }); } template requires (!std::is_void_v) Promise then(utils::MiniFunction&& callback) { if (m_data->cancelled) { return make_cancelled(); } std::unique_lock _(m_data->mutex); // Check if this Promise has already been resolved, and if so // immediately queue the callback with the value if (m_data->result.has_value()) { auto v = m_data->result.value(); if (v.index() == 0) { Loader::get()->queueInMainThread([callback = std::move(callback), ok = std::move(std::get<0>(v))] { callback(ok); }); } return make_cancelled(); } return make_fwd(callback, m_data); } template requires (!std::is_void_v) Promise then(utils::MiniFunction(Result)>&& callback) { if (m_data->cancelled) { return make_cancelled(); } std::unique_lock _(m_data->mutex); // Check if this Promise has already been resolved, and if so // immediately queue the callback with the value if (m_data->result.has_value()) { auto v = m_data->result.value(); if (v.index() == 0) { Loader::get()->queueInMainThread([callback = std::move(callback), ok = std::move(std::get<0>(v))] { (void)callback(Ok(ok)); }); } else { Loader::get()->queueInMainThread([callback = std::move(callback), err = std::move(std::get<1>(v))] { (void)callback(Err(err)); }); } return make_cancelled(); } return Promise([data = m_data, callback](auto fwdStateToNextPromise, auto const& nextPromiseCancelled) { data->callback = [&nextPromiseCancelled, fwdStateToNextPromise, callback](auto&& state) { // Can't use std::visit if Value and Error are the same >:( switch (state.index()) { case STATE_VALUE_INDEX: { auto mapped = callback(Ok(std::move(std::get(state)))); if (mapped) { fwdStateToNextPromise(Promise::State( std::in_place_index, std::move(mapped).unwrap() )); } else { fwdStateToNextPromise(Promise::State( std::in_place_index, std::move(mapped).unwrapErr() )); } } break; case STATE_ERROR_INDEX: { auto mapped = callback(Err(std::move(std::get(state)))); if (mapped) { fwdStateToNextPromise(Promise::State( std::in_place_index, std::move(mapped).unwrap() )); } else { fwdStateToNextPromise(Promise::State( std::in_place_index, std::move(mapped).unwrapErr() )); } } break; case STATE_PROGRESS_INDEX: { fwdStateToNextPromise(Promise::State( std::in_place_index, std::move(std::get(state)) )); } break; case STATE_CANCELLED_INDEX: { fwdStateToNextPromise(Promise::State( std::in_place_index, std::move(std::get(state)) )); } break; } }; }, m_data->shouldStartThreaded, std::monostate()); } Promise expect(utils::MiniFunction&& callback) { return this->template expect([callback](auto error) { callback(error); return std::move(error); }); } template requires (!std::is_void_v) Promise expect(utils::MiniFunction&& callback) { if (m_data->cancelled) { return make_cancelled(); } std::unique_lock _(m_data->mutex); // Check if this Promise has already been resolved, and if so // immediately queue the callback with the value if (m_data->result.has_value()) { auto v = m_data->result.value(); if (v.index() == 1) { Loader::get()->queueInMainThread([callback = std::move(callback), err = std::move(std::get<1>(v))] { callback(err); }); } return make_cancelled(); } return make_fwd(callback, m_data); } Promise progress(utils::MiniFunction&& callback) { return this->template progress([callback](auto prog) { callback(prog); return std::move(prog); }); } template requires (!std::is_void_v) Promise progress(utils::MiniFunction&& callback) { if (m_data->cancelled) { return make_cancelled(); } std::unique_lock _(m_data->mutex); // Check if this Promise has already been resolved if (m_data->result.has_value()) { return make_cancelled(); } return make_fwd(callback, m_data); } Promise finally(utils::MiniFunction&& callback) { if (m_data->cancelled) { return make_cancelled(); } std::unique_lock _(m_data->mutex); // Check if this Promise has already been resolved, and if so // immediately queue the callback with the value if (m_data->result.has_value()) { Loader::get()->queueInMainThread([callback = std::move(callback)] { callback(); }); return make_cancelled(); } return Promise([data = m_data, callback](auto fwdStateToNextPromise) { data->callback = [fwdStateToNextPromise, callback](auto&& state) { if (state.index() == STATE_VALUE_INDEX || state.index() == STATE_ERROR_INDEX) { callback(); } fwdStateToNextPromise(state); }; }); } Promise cancelled(utils::MiniFunction&& callback) { if (m_data->cancelled) { Loader::get()->queueInMainThread([callback = std::move(callback)] { callback(); }); return make_cancelled(); } std::unique_lock _(m_data->mutex); if (m_data->result.has_value()) { return make_cancelled(); } return Promise([data = m_data, callback](auto fwdStateToNextPromise) { data->callback = [fwdStateToNextPromise, callback](auto&& state) { if (state.index() == STATE_CANCELLED_INDEX) { callback(); } fwdStateToNextPromise(state); }; }); } void resolve(Value&& value) { invoke_callback(State(std::in_place_index, std::move(value)), m_data); } void reject(Error&& error) { invoke_callback(State(std::in_place_index, std::move(error)), m_data); } void cancel() { m_data->cancelled = true; invoke_callback(State(std::in_place_index, CancelledState()), m_data); } /** * Returns a filter for listening to this `Promise` through the Geode * Events system. Useful for example for using `Promise`s on layers, * which may be removed from the node tree before the `Promise` * finishes and as such calling a `then` callback that captures the * layer would then read undefined memory */ PromiseEventFilter listen(); private: struct Data final { std::mutex mutex; OnStateChange callback; std::optional> result; std::atomic_bool cancelled; bool shouldStartThreaded; }; std::shared_ptr m_data; template static Promise make_cancelled() { auto ret = Promise(); ret.cancel(); return std::move(ret); } template static Promise make_fwd(auto mapper, std::shared_ptr data) { return Promise([data, mapper](auto fwdStateToNextPromise, auto const&) { data->callback = [fwdStateToNextPromise, mapper](auto&& state) { if (state.index() == Ix) { auto mapped = mapper(std::get(state)); fwdStateToNextPromise(Promise::State( std::in_place_index, mapped )); } // Can't use std::visit if Value and Error are the same >:( else switch (state.index()) { case STATE_VALUE_INDEX: if constexpr (Ix != STATE_VALUE_INDEX) { fwdStateToNextPromise(Promise::State( std::in_place_index, std::move(std::get(state)) )); } break; case STATE_ERROR_INDEX: if constexpr (Ix != STATE_ERROR_INDEX) { fwdStateToNextPromise(Promise::State( std::in_place_index, std::move(std::get(state)) )); } break; case STATE_PROGRESS_INDEX: if constexpr (Ix != STATE_PROGRESS_INDEX) { fwdStateToNextPromise(Promise::State( std::in_place_index, std::move(std::get(state)) )); } break; case STATE_CANCELLED_INDEX: if constexpr (Ix != STATE_CANCELLED_INDEX) { fwdStateToNextPromise(Promise::State( std::in_place_index, std::move(std::get(state)) )); } break; } }; }, data->shouldStartThreaded, std::monostate()); } static void invoke_callback(State&& state, std::shared_ptr data) { if (data->cancelled) return; std::unique_lock _(data->mutex); if (data->callback) { Loader::get()->queueInMainThread([callback = data->callback, state = State(state)]() { callback(state); }); } // Store the state to let future installed callbacks be immediately resolved if (state.index() == STATE_VALUE_INDEX) { data->result = std::variant(std::in_place_index<0>, std::get<0>(std::move(state))); } else if (state.index() == STATE_ERROR_INDEX) { data->result = std::variant(std::in_place_index<1>, std::get<1>(std::move(state))); } else if (state.index() == STATE_CANCELLED_INDEX) { data->cancelled = true; } } static void invoke_source(utils::MiniFunction&& source, std::shared_ptr data) { source( [data](auto&& state) { invoke_callback(std::move(state), data); }, data->cancelled ); } }; /** * Wraps a `Promise` in the Geode Event system for easier consumption. * Useful for example for layers, where just regularly waiting for the * `Promise` could run into issues if the layer is freed from memory; * whereas with event listeners being RAII, they are automatically * removed from layers, avoiding use-after-free errors */ template class PromiseEvent : public Event { protected: size_t m_id; std::variant m_value; PromiseEvent(size_t id, std::variant&& value) : m_id(id), m_value(value) {} friend class Promise; friend class PromiseEventFilter; public: T const* getResolve() const { return std::get_if<0>(&m_value); } E const* getReject() const { return std::get_if<1>(&m_value); } P const* getProgress() const { return std::get_if<2>(&m_value); } bool isFinally() const { return m_value.index() != 2; } }; template class PromiseEventFilter : public EventFilter> { public: using Callback = void(PromiseEvent*); protected: size_t m_id; friend class Promise; PromiseEventFilter(size_t id) : m_id(id) {} public: PromiseEventFilter() : m_id(0) {} ListenerResult handle(utils::MiniFunction fn, PromiseEvent* event) { // log::debug("Event mod filter: {}, {}, {}, {}", m_mod, static_cast(m_type), event->getMod(), static_cast(event->getType())); if (m_id == event->m_id) { fn(event); } return ListenerResult::Propagate; } }; template PromiseEventFilter Promise::listen() { // After 4 billion promises this will overflow and start producing // the same IDs again, so technically if some promise takes // literally forever then this could cause issues later on static size_t ID_COUNTER = 0; ID_COUNTER += 1; // Reserve 0 for PromiseEventFilter not listening to anything if (ID_COUNTER == 0) { ID_COUNTER += 1; } size_t id = ID_COUNTER; this ->then([id](auto&& value) { PromiseEvent(id, std::variant { std::in_place_index<0>, std::forward(value) }).post(); }) .expect([id](auto&& error) { PromiseEvent(id, std::variant { std::in_place_index<1>, std::forward(error) }).post(); }) .progress([id](auto&& prog) { PromiseEvent(id, std::variant { std::in_place_index<2>, std::forward

(prog) }).post(); }); return PromiseEventFilter(id); } }