├── .idea ├── iara.iml ├── codeStyles │ └── codeStyleConfig.xml ├── .gitignore ├── vcs.xml ├── modules.xml ├── customTargets.xml └── misc.xml ├── utils ├── src │ ├── logger.cpp │ └── format.cpp └── include │ └── utils │ ├── inclusion.hpp │ ├── object-buffer.hpp │ ├── format.hpp │ ├── object-pool.hpp │ ├── resource-guard.hpp │ ├── pool-allocator.hpp │ ├── storage-for.hpp │ ├── type-traits.hpp │ ├── logger.hpp │ ├── circular-queue.hpp │ ├── test-helpers.hpp │ └── types.hpp ├── plumbing └── include │ └── plumbing │ ├── pipe-guard.hpp │ ├── proxy.hpp │ ├── source.hpp │ ├── duplex.hpp │ ├── sink.hpp │ └── box.hpp ├── fugax ├── include │ ├── fugax.hpp │ └── fugax │ │ ├── event-listener.hpp │ │ ├── event-guard.hpp │ │ ├── event.hpp │ │ └── event-loop.hpp ├── src │ ├── event-guard.cpp │ ├── event.cpp │ └── event-loop.cpp └── README.md ├── test ├── include │ └── test │ │ ├── juro │ │ └── helpers.hpp │ │ └── fugax │ │ └── helpers.hpp └── src │ ├── fuss │ └── test.cpp │ └── fugax │ └── test.cpp ├── juro ├── src │ ├── compose │ │ └── all.cpp │ └── promise.cpp └── include │ └── juro │ ├── compose │ ├── race.hpp │ └── all.hpp │ ├── factories.hpp │ ├── helpers.hpp │ └── promise.hpp ├── CMakePresets.json ├── LICENSE ├── .github └── workflows │ └── cmake.yml ├── config └── include │ └── config │ └── fugax.hpp.in ├── fuss ├── README.md └── include │ └── fuss.hpp ├── .gitignore ├── CMakeLists.txt └── README.md /.idea/iara.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /utils/src/logger.cpp: -------------------------------------------------------------------------------- 1 | #include "utils/logger.hpp" 2 | 3 | namespace utils { 4 | constexpr const char *logger_scope::entry_tags[]; 5 | } /* namespace utils */ 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /plumbing/include/plumbing/pipe-guard.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PLUMBING_PIPE_GUARD_HPP 2 | #define PLUMBING_PIPE_GUARD_HPP 3 | 4 | namespace plumbing { 5 | 6 | 7 | 8 | } /* namespace plumbing */ 9 | 10 | #endif /* PLUMBING_PIPE_GUARD_HPP */ -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /fugax/include/fugax.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file fugax/include/fugax.hpp 3 | * @brief Main include header for Fugax 4 | * @author kazeshi 5 | * @date 22/06/23 6 | * @copyright (C) 2023 kazeshi 7 | **/ 8 | 9 | #ifndef FUGAX_HPP 10 | #define FUGAX_HPP 11 | 12 | #include "fugax/event-loop.hpp" 13 | 14 | #endif /* FUGAX_HPP */ 15 | -------------------------------------------------------------------------------- /.idea/customTargets.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/include/test/juro/helpers.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file test/include/test/juro/helpers.hpp 3 | * @brief Helper functions and structures for testing purposes 4 | * @author André Medeiros 5 | * @date 31/07/23 6 | * @copyright 2023 (C) André Medeiros 7 | **/ 8 | #ifndef JURO_TEST_HELPERS_HPP 9 | #define JURO_TEST_HELPERS_HPP 10 | 11 | namespace juro::test::helpers { 12 | 13 | } /* namespace juro::test::helpers */ 14 | 15 | #endif /* JURO_TEST_HELPERS_HPP */ -------------------------------------------------------------------------------- /utils/include/utils/inclusion.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_INCLUSION_HPP 2 | #define UTILS_INCLUSION_HPP 3 | 4 | namespace utils { 5 | 6 | template 7 | struct includes { 8 | static constexpr bool value = false; 9 | }; 10 | 11 | template 12 | struct includes { 13 | static constexpr bool value = 14 | std::is_same::value || includes::value; 15 | }; 16 | 17 | } /* namespace utils */ 18 | 19 | #endif /* UTILS_INCLUSION_HPP */ -------------------------------------------------------------------------------- /utils/src/format.cpp: -------------------------------------------------------------------------------- 1 | #include "utils/format.hpp" 2 | 3 | #ifndef _GNU_SOURCE 4 | #error "Macro _GNU_SOURCE must be defined for formatting support" 5 | #endif 6 | 7 | namespace utils { 8 | 9 | [[gnu::format(printf, 1, 2)]] 10 | std::string format(const char *fmt, ...) { 11 | std::va_list scan_args; 12 | va_start(scan_args, fmt); 13 | 14 | char *formatted; 15 | std::size_t size = vasprintf(&formatted, fmt, scan_args); 16 | std::string result { formatted, size }; 17 | free(formatted); 18 | return result; 19 | } 20 | 21 | } /* namespace utils */ -------------------------------------------------------------------------------- /juro/src/compose/all.cpp: -------------------------------------------------------------------------------- 1 | #include "juro/promise.hpp" 2 | #include "juro/compose/all.hpp" 3 | 4 | namespace juro::compose { 5 | 6 | void void_settler(const promise_ptr &promise, const promise_ptr &all_promise, const std::shared_ptr &counter) { 7 | promise->then([counter, all_promise] { 8 | if(--(*counter) == 0 && all_promise->is_pending()) { 9 | all_promise->resolve(); 10 | } 11 | }, [all_promise] (std::exception_ptr error) { 12 | if(all_promise->is_pending()) { 13 | all_promise->reject(error); 14 | } 15 | }); 16 | } 17 | 18 | } /* namespace juro::compose */ -------------------------------------------------------------------------------- /fugax/src/event-guard.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file fugax/src/event-guard.cpp 3 | * @brief Implementation of non-templated event guard functions 4 | * @author André Medeiros 5 | * @date 22/06/23 6 | * @copyright 2023 (C) André Medeiros 7 | **/ 8 | 9 | #include "fugax/event-guard.hpp" 10 | 11 | namespace fugax { 12 | 13 | event_guard &event_guard::operator=(event_guard &&other) noexcept { 14 | release(); 15 | listener = std::move(other.listener); 16 | return *this; 17 | } 18 | 19 | void event_guard::release() const noexcept { 20 | if(const auto target = listener.lock()) { 21 | target->cancel(); 22 | } 23 | } 24 | 25 | } /* namespace fugax */ -------------------------------------------------------------------------------- /fugax/include/fugax/event-listener.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file fugax/include/fugax/event-listener.hpp 3 | * @brief Contains the definition of event listeners 4 | * @author André Medeiros 5 | * @date 22/06/23 6 | * @copyright 2023 (C) André Medeiros 7 | **/ 8 | 9 | #ifndef FUGAX_EVENT_LISTENER_HPP 10 | #define FUGAX_EVENT_LISTENER_HPP 11 | 12 | #include 13 | #include "event.hpp" 14 | 15 | namespace fugax { 16 | 17 | /** 18 | * @brief An event listener is a handle that can be used to cancel an event, 19 | * given it hasn't been executed yet 20 | */ 21 | using event_listener = std::weak_ptr; 22 | 23 | } /* namespace fugax */ 24 | 25 | #endif /* FUGAX_EVENT_LISTENER_HPP */ 26 | -------------------------------------------------------------------------------- /utils/include/utils/object-buffer.hpp: -------------------------------------------------------------------------------- 1 | #ifndef OBJECT_BUFFER_HPP 2 | #define OBJECT_BUFFER_HPP 3 | 4 | template 5 | struct object_buffer { 6 | alignas(T) unsigned char data[sizeof(T)]; 7 | 8 | template 9 | T &construct(T_args &... args) { 10 | return *new(reinterpret_cast(&data)) T(args ...); 11 | } 12 | 13 | void destruct() { 14 | reinterpret_cast(&data)->~T(); 15 | } 16 | 17 | operator T &() const { 18 | return *reinterpret_cast(&data); 19 | } 20 | 21 | operator T &() { 22 | return *reinterpret_cast(&data); 23 | } 24 | }; 25 | 26 | #endif /* OBJECT_BUFFER_HPP */ -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 6, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 25, 6 | "patch": 0 7 | }, 8 | "configurePresets": [ 9 | { 10 | "name": "default", 11 | "displayName": "Default configure preset", 12 | "description": "A preset with sensible default values that can be overridden", 13 | "cacheVariables": { 14 | "FUGAX_TIME_INCLUDE": "", 15 | "FUGAX_TIME_TYPE": "std::uint_fast32_t", 16 | "FUGAX_MUTEX_INCLUDE": "", 17 | "FUGAX_MUTEX_TYPE": "std::mutex" 18 | } 19 | } 20 | ], 21 | "buildPresets": [ 22 | { 23 | "name": "default", 24 | "displayName": "Default build preset", 25 | "configurePreset": "default" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /juro/src/promise.cpp: -------------------------------------------------------------------------------- 1 | #include "juro/promise.hpp" 2 | 3 | namespace juro { 4 | 5 | promise_interface::promise_interface(promise_state state) noexcept : 6 | state { state } 7 | { } 8 | 9 | void promise_interface::set_settle_handler(std::function &&handler) noexcept { 10 | on_settle = std::move(handler); 11 | if(is_settled()) { 12 | on_settle(); 13 | } 14 | } 15 | 16 | void promise_interface::resolved() noexcept { 17 | state = promise_state::RESOLVED; 18 | if(on_settle) { 19 | on_settle(); 20 | } 21 | } 22 | 23 | void promise_interface::rejected() { 24 | state = promise_state::REJECTED; 25 | if(on_settle) { 26 | on_settle(); 27 | } else { 28 | throw promise_error { "Unhandled promise rejection" }; 29 | } 30 | } 31 | 32 | } /* namespace juro */ -------------------------------------------------------------------------------- /fugax/src/event.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file fugax/src/event.cpp 3 | * @brief Implementation of non-templated event functions 4 | * @author André Medeiros 5 | * @date 26/06/23 6 | * @copyright 2023 (C) André Medeiros 7 | */ 8 | 9 | #include "fugax/event.hpp" 10 | 11 | namespace fugax { 12 | 13 | void event_handler::operator()(event &ev) const { handler->invoke(ev); } 14 | 15 | event::event(event_handler &&handler, time_type interval, time_type due_time, bool recurring) : 16 | handler { std::forward(handler) }, 17 | interval { interval }, 18 | due_time { due_time }, 19 | recurring { recurring } 20 | { } 21 | 22 | void event::fire() { handler(*this); } 23 | void event::cancel() noexcept { cancelled = true; } 24 | void event::reschedule(time_type time_point) noexcept { due_time = time_point; } 25 | 26 | } /* namespace fugax */ -------------------------------------------------------------------------------- /utils/include/utils/format.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_FORMAT_HPP 2 | #define UTILS_FORMAT_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "types.hpp" 9 | 10 | namespace utils { 11 | 12 | [[gnu::format(printf, 1, 2)]] 13 | std::string format(const char *fmt, ...); 14 | 15 | 16 | template 17 | class formatter { 18 | const char *fmt; 19 | 20 | public: 21 | explicit constexpr formatter(const char *fmt) noexcept : 22 | fmt { fmt } 23 | { } 24 | 25 | formatter(const formatter &) noexcept = default; 26 | formatter(formatter &&) noexcept = default; 27 | 28 | formatter &operator=(const formatter &) noexcept = default; 29 | formatter &operator=(formatter &&) noexcept = default; 30 | virtual ~formatter() noexcept = default; 31 | 32 | std::string operator()(const T_args &...args) const { 33 | return format(fmt, args...); 34 | } 35 | }; 36 | 37 | } /* namespace utils */ 38 | 39 | #endif /* UTILS_FORMAT_HPP */ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 André Medeiros 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /utils/include/utils/object-pool.hpp: -------------------------------------------------------------------------------- 1 | #ifndef OBJECT_POOL_HPP 2 | #define OBJECT_POOL_HPP 3 | 4 | 5 | #include "object-buffer.hpp" 6 | #include "circular-queue.hpp" 7 | #include 8 | 9 | template 10 | class object_pool { 11 | 12 | using object = object_buffer; 13 | static constexpr std::size_t factor = 1< blocks; 16 | circular_queue queue; 17 | 18 | public: 19 | object_pool() : capacity(factor) { 20 | object *block = new object[factor]; 21 | blocks.push_back(block); 22 | for(auto i = 0; i < factor; i++) queue.push(&block[i]); 23 | } 24 | 25 | ~object_pool() { 26 | for(auto block : blocks) delete[] block; 27 | } 28 | 29 | T *allocate() { 30 | if(queue.get_count() == 0) { 31 | object *block = new object[capacity]; 32 | blocks.push_back(block); 33 | for(auto i = 0; i < capacity; i++) queue.push(&block[i]); 34 | capacity *= 2; 35 | } 36 | 37 | return reinterpret_cast(queue.pop()); 38 | } 39 | 40 | void deallocate(T *obj) { 41 | queue.push(reinterpret_cast(obj)); 42 | } 43 | 44 | }; 45 | 46 | #endif /* OBJECT_POOL_HPP */ -------------------------------------------------------------------------------- /utils/include/utils/resource-guard.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_RESOURCE_GUARD_HPP 2 | #define UTILS_RESOURCE_GUARD_HPP 3 | 4 | #include 5 | 6 | namespace utils { 7 | 8 | template 9 | class resource_guard { 10 | std::optional resource; 11 | 12 | public: 13 | resource_guard() : resource { std::nullopt } { } 14 | resource_guard(T_resource &&resource) : 15 | resource { std::move(resource) } 16 | { } 17 | resource_guard(resource_guard &&other) : 18 | resource { std::move(other.resource) } 19 | { other.resource.reset(); } 20 | resource_guard(const resource_guard &) = delete; 21 | ~resource_guard() { if(resource) resource->release(); } 22 | 23 | resource_guard& operator=(T_resource &&other) { 24 | if(resource) resource->release(); 25 | resource.emplace(std::move(other)); 26 | return *this; 27 | } 28 | 29 | void reset() noexcept { 30 | if(resource) { 31 | resource->release(); 32 | resource.reset(); 33 | } 34 | } 35 | 36 | resource_guard &operator=(resource_guard &&) = default; 37 | resource_guard &operator=(const resource_guard &) = delete; 38 | operator bool() const { return resource.has_value(); } 39 | }; 40 | 41 | } /* namespace utils */ 42 | 43 | #endif /* UTILS_RESOURCE_GUARD_HPP */ -------------------------------------------------------------------------------- /plumbing/include/plumbing/proxy.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PLUMBING_PROXY_HPP 2 | #define PLUMBING_PROXY_HPP 3 | 4 | #include "duplex.hpp" 5 | 6 | namespace plumbing { 7 | 8 | namespace proxy { 9 | 10 | 11 | template 12 | struct source : public virtual stream::source { 13 | source(stream::source &target) : stream::source() { 14 | target.listen>([this](const T &data) { 15 | this->produce(data); 16 | }); 17 | } 18 | }; 19 | 20 | template 21 | struct sink : public virtual stream::sink { 22 | stream::sink ⌖ 23 | 24 | using stream::sink::consume; 25 | 26 | sink(stream::sink &target) : stream::sink(), target(target) { } 27 | 28 | void consume(const T &data) { 29 | this->target.consume(data); 30 | } 31 | }; 32 | 33 | template 34 | struct duplex : 35 | public stream::duplex, public sink, public source { 36 | 37 | template 38 | duplex(T_target &target) : 39 | sink(target), source(target) { } 40 | 41 | template 42 | duplex(T_target_in &target_in, T_target_out &target_out) : 43 | sink(target_in), source(target_out) { } 44 | }; 45 | 46 | 47 | } /* namespace proxy */ 48 | 49 | } /* namespace plumbing */ 50 | 51 | #endif /* PLUMBING_PROXY_HPP */ -------------------------------------------------------------------------------- /plumbing/include/plumbing/source.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PLUMBING_SOURCE_HPP 2 | #define PLUMBING_SOURCE_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace plumbing { 11 | 12 | namespace messages { 13 | namespace source { 14 | template 15 | struct data_available : public fuss::message { }; 16 | } 17 | } 18 | 19 | template class sink; 20 | 21 | template 22 | class source : private fuss::shouter> { 23 | template friend class sink; 24 | public: 25 | using type_out = T; 26 | 27 | virtual ~source() = default; 28 | 29 | virtual void produce(const T &data) { 30 | this->template shout>(data); 31 | } 32 | 33 | template()))> 34 | void produce(T_collection &&data) { 35 | for(auto &&datum : data) { 36 | produce(std::forward(datum)); 37 | } 38 | } 39 | 40 | virtual void pipe_to(sink &sink) { 41 | sink.pipe_from(*this); 42 | } 43 | 44 | 45 | template 46 | T_sink &operator>>(T_sink &sink) { 47 | sink.pipe_from(*this); 48 | return sink; 49 | } 50 | }; 51 | 52 | 53 | } /* namespace plumbing */ 54 | 55 | #endif /* PLUMBING_SOURCE_HPP */ -------------------------------------------------------------------------------- /.github/workflows/cmake.yml: -------------------------------------------------------------------------------- 1 | name: CMake 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) 11 | BUILD_TYPE: Release 12 | 13 | jobs: 14 | build: 15 | # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. 16 | # You can convert this to a matrix build if you need cross-platform coverage. 17 | # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - name: Configure CMake 24 | # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. 25 | # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type 26 | run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} --preset=default 27 | 28 | - name: Build 29 | # Build your program with the given configuration 30 | run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} 31 | 32 | - name: Test 33 | working-directory: ${{github.workspace}}/build 34 | # Execute tests defined by the CMake configuration. 35 | # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail 36 | run: ctest -C ${{env.BUILD_TYPE}} 37 | 38 | -------------------------------------------------------------------------------- /utils/include/utils/pool-allocator.hpp: -------------------------------------------------------------------------------- 1 | #ifndef POOL_ALLOCATOR_HPP 2 | #define POOL_ALLOCATOR_HPP 3 | 4 | #include "object-pool.hpp" 5 | 6 | template 7 | struct pool_allocator { 8 | 9 | using value_type = T; 10 | using reference = T&; 11 | using const_reference = const T&; 12 | using pointer = T*; 13 | using const_pointer = const T*; 14 | using size_type = std::size_t; 15 | using difference_type = std::ptrdiff_t; 16 | 17 | static object_pool pool; 18 | 19 | pool_allocator() { }; 20 | 21 | template 22 | pool_allocator(const pool_allocator& other) { } 23 | 24 | T *allocate(std::size_t len) { 25 | if(len > 1) throw std::bad_alloc(); 26 | return pool.allocate(); 27 | } 28 | 29 | void deallocate(T *obj, std::size_t len) { 30 | pool.deallocate(obj); 31 | } 32 | 33 | void construct(pointer p, const_reference val) { 34 | new(p) T(val); 35 | } 36 | 37 | void destroy(pointer p) { 38 | p->~T(); 39 | } 40 | 41 | size_type max_size() const noexcept { return 1; } 42 | 43 | template 44 | struct rebind { 45 | using other = pool_allocator; 46 | }; 47 | }; 48 | 49 | template 50 | object_pool pool_allocator::pool; 51 | 52 | 53 | #endif /* POOL_ALLOCATOR_HPP */ -------------------------------------------------------------------------------- /config/include/config/fugax.hpp.in: -------------------------------------------------------------------------------- 1 | /** 2 | * @file config/include/config/fugax.hpp 3 | * @brief Configuration options for Fugax 4 | * @author André Medeiros 5 | * @date 22/06/23 6 | * @copyright 2023 (C) André Medeiros 7 | **/ 8 | 9 | #ifndef FUGAX_CONFIG_HPP 10 | #define FUGAX_CONFIG_HPP 11 | 12 | #include 13 | 14 | #cmakedefine FUGAX_TIME_INCLUDE 15 | #ifdef FUGAX_TIME_INCLUDE 16 | /** 17 | * @brief Header that provides the informed time typedef 18 | */ 19 | #include @FUGAX_TIME_INCLUDE@ 20 | #endif /* FUGAX_TIME_INCLUDE */ 21 | 22 | #cmakedefine FUGAX_MUTEX_INCLUDE 23 | #ifdef FUGAX_MUTEX_INCLUDE 24 | /** 25 | * @brief Header that provides the informed mutex typedef 26 | */ 27 | #include @FUGAX_MUTEX_INCLUDE@ 28 | #endif /* FUGAX_MUTEX_INCLUDE */ 29 | 30 | namespace config::fugax { 31 | 32 | /** 33 | * @brief The type of the internal counter; should alias an 34 | * unsigned integer type 35 | */ 36 | using time_type = @FUGAX_TIME_TYPE@; 37 | static_assert( 38 | std::is_unsigned_v, 39 | "`time_type` must be of unsigned integer type" 40 | ); 41 | 42 | /** 43 | * @brief The type of mutex used to lock the event loop; 44 | * must meet `BasicLockable` requirements 45 | */ 46 | using mutex_type = @FUGAX_MUTEX_TYPE@; 47 | static_assert( 48 | std::conjunction_v< 49 | std::is_invocable_r, 50 | std::is_invocable_r 51 | >, 52 | "The provided type does not meet `BasicLockable` requirements" 53 | ); 54 | 55 | } /* namespace config::fugax */ 56 | 57 | #endif /* FUGAX_CONFIG_HPP */ 58 | -------------------------------------------------------------------------------- /utils/include/utils/storage-for.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_STORAGE_FOR_HPP 2 | #define UTILS_STORAGE_FOR_HPP 3 | 4 | #include 5 | 6 | namespace utils { 7 | 8 | template 9 | class storage_for { 10 | union storage_space { 11 | T_object object; 12 | struct empty_storage { } empty; 13 | ~storage_space() { } 14 | } storage; 15 | 16 | public: 17 | storage_for() : storage { .empty { } } { } 18 | storage_for(auto && ...args) : 19 | storage { T_object { std::forward(args)... } } 20 | { } 21 | ~storage_for() = default; 22 | storage_for(const storage_for &) = delete; 23 | storage_for(storage_for &&) = delete; 24 | storage_for &operator=(const storage_for &) = delete; 25 | storage_for &operator=(storage_for &&) = delete; 26 | 27 | T_object *construct(auto && ...args) { 28 | return new (&storage.object) 29 | T_object { std::forward(args)... }; 30 | } 31 | 32 | storage_for *destruct() noexcept { 33 | storage.object.~T_object(); 34 | new (&storage.empty) typename storage_space::empty_storage { }; 35 | return this; 36 | } 37 | 38 | T_object extract() noexcept { 39 | T_object extracted { std::move(storage.object) }; 40 | destruct(); 41 | return extracted; 42 | } 43 | 44 | T_object &operator->() noexcept { return storage.object; } 45 | 46 | inline static storage_for &from(T_object *object) noexcept { 47 | return *reinterpret_cast *>(object); 48 | } 49 | }; 50 | 51 | } /* namespace utils */ 52 | 53 | #endif /* UTILS_STORAGE_FOR_HPP */ -------------------------------------------------------------------------------- /test/include/test/fugax/helpers.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file test/include/test/fugax/helpers.hpp 3 | * @brief Helper functions and structures for testing purposes 4 | * @author André Medeiros 5 | * @date 31/07/23 6 | * @copyright 2023 (C) André Medeiros 7 | **/ 8 | 9 | #ifndef FUGAX_TEST_HELPERS_HPP 10 | #define FUGAX_TEST_HELPERS_HPP 11 | 12 | #include "catch2/catch_test_macros.hpp" 13 | #include "fugax/event-loop.hpp" 14 | #include "utils/test-helpers.hpp" 15 | 16 | namespace fugax::test::helpers { 17 | 18 | using namespace utils::test_helpers; 19 | 20 | template 21 | void schedule_for_test(T_launcher &&launcher, T_then &&then) { 22 | static_assert(std::is_invocable_v, "Launcher must be invocable"); 23 | auto result = attempt(launcher); 24 | 25 | THEN("no exception must have been thrown") { 26 | REQUIRE_FALSE(result.has_error()); 27 | } 28 | 29 | THEN("a valid event listener must be returned") { 30 | REQUIRE(result.template holds_value()); 31 | 32 | auto &listener = result.get_value(); 33 | REQUIRE_FALSE(listener.expired()); 34 | 35 | then(listener); 36 | } 37 | } 38 | 39 | struct test_clock { 40 | using time_type = fugax::time_type; 41 | time_type counter = 0; 42 | 43 | inline test_clock &advance(time_type by) noexcept { 44 | counter += by; 45 | return *this; 46 | } 47 | 48 | inline test_clock ®ress(time_type by) noexcept { 49 | counter -= by; 50 | return *this; 51 | } 52 | 53 | constexpr operator time_type() const noexcept { 54 | return counter; 55 | } 56 | }; 57 | 58 | } /* namespace fugax::test::helpers */ 59 | 60 | #endif /* FUGAX_TEST_HELPERS_HPP */ 61 | -------------------------------------------------------------------------------- /fuss/README.md: -------------------------------------------------------------------------------- 1 | # FUSS 2 | 3 | FUSS is a simple and efficient pub/sub pattern implementation aimed for C++17 programs with an intuitive interface. 4 | It consists of a single header file and has no external dependencies. 5 | 6 | ## Features 7 | 8 | - Simple, efficient, small and elegant. Just read the code! 9 | - Leverages modern C++ to provide an intuitive and pleasant API. 10 | - Avoids unnecessary dynamic memory as much as possible. 11 | - No virtual functions. 12 | - No RTTI. 13 | 14 | ## Example 15 | 16 | ```C++ 17 | #include 18 | 19 | // Define what messages are available to be published. 20 | struct greeting : fuss::message<> {}; 21 | 22 | // Note that messages can have parameters. 23 | struct echo : fuss::message {}; 24 | 25 | // Create a publisher object 26 | struct noisy : fuss::shouter { 27 | void greet() { 28 | // Publishes the `greeting` message. 29 | // `shout()` is protected, so we create a member function to call it 30 | shout(); 31 | } 32 | 33 | void repeat(const std::string &s) { 34 | // Publishes the `echo` message with a string as parameter 35 | shout(s); 36 | } 37 | }; 38 | 39 | // Instantiate the publisher 40 | noisy n; 41 | 42 | // Subscribe to the messages 43 | auto greeting_listener = n.listen([]() { 44 | std::cout << "Hello world!" << std::endl; 45 | }); 46 | 47 | n.listen([](std::string s) { 48 | std::cout << s << std::endl; 49 | }); 50 | 51 | n.greet(); // prints 'Hello world!' 52 | n.repeat("What a fuss!"); // prints 'What a fuss!' 53 | 54 | // Unsubscribe from a message 55 | n.unlisten(greeting_listener); 56 | 57 | n.greet(); // doesn't print anything 58 | 59 | ``` 60 | 61 | ## Copyright 62 | 63 | Copyright André Medeiros 2022 64 | Contact: andre@setadesenvolvimentos.com.br -------------------------------------------------------------------------------- /plumbing/include/plumbing/duplex.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PLUMBING_DUPLEX_HPP 2 | #define PLUMBING_DUPLEX_HPP 3 | 4 | #include "source.hpp" 5 | #include "sink.hpp" 6 | #include 7 | 8 | namespace plumbing { 9 | 10 | template 11 | struct duplex : public virtual sink, public virtual source { 12 | using type_in = T_in; 13 | using type_out = T_out; 14 | }; 15 | 16 | template 17 | class transform : public duplex { 18 | 19 | using transform_function = std::function; 20 | transform_function apply; 21 | 22 | public: 23 | transform(transform_function apply) : apply(apply) { } 24 | 25 | void consume(const T_in &data) final { 26 | this->produce(this->apply(data)); 27 | } 28 | }; 29 | 30 | template 31 | class buffer : 32 | public duplex, 33 | public buffered_sink { 34 | 35 | protected: 36 | void pipe_to(sink &target) final { 37 | try { 38 | auto &active = dynamic_cast &>(target); 39 | active.listen( 40 | [this](const std::size_t count) { 41 | this->next(count); 42 | } 43 | ); 44 | } catch(std::bad_cast &error) { } 45 | source::pipe_to(target); 46 | } 47 | 48 | public: 49 | void requested_data(const T &data) { 50 | this->produce(data); 51 | } 52 | }; 53 | 54 | 55 | template 56 | class splitter : public duplex, T> { 57 | 58 | void consume(const std::vector &vector) override { 59 | for(const T &element : vector) { 60 | this->produce(element); 61 | } 62 | } 63 | }; 64 | 65 | 66 | class string_splitter : public duplex { 67 | 68 | void consume(const std::string &vector) override { 69 | for(const auto &element : vector) { 70 | this->produce(element); 71 | } 72 | } 73 | }; 74 | 75 | 76 | } /* namespace plumbing */ 77 | 78 | #endif /* PLUMBING_DUPLEX_HPP */ -------------------------------------------------------------------------------- /utils/include/utils/type-traits.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_TYPE_TRAITS_HPP 2 | #define UTILS_TYPE_TRAITS_HPP 3 | 4 | #include 5 | #include 6 | 7 | namespace utils::type_traits { 8 | template 9 | struct is_list_constructible_impl : std::false_type { }; 10 | 11 | template 12 | struct is_list_constructible_impl< 13 | T_object, 14 | std::void_t()... })>, 15 | T_args ... 16 | > : std::true_type { }; 17 | 18 | template 19 | struct is_list_constructible : std::integral_constant< 20 | bool, 21 | is_list_constructible_impl::value 22 | > { }; 23 | template 24 | inline constexpr bool is_list_constructible_v = 25 | is_list_constructible::value; 26 | 27 | template 28 | struct contains; 29 | 30 | template 31 | struct contains> : std::integral_constant< 32 | bool, 33 | std::disjunction_v...> 34 | > { }; 35 | 36 | template 37 | constexpr inline bool contains_v = contains::value; 38 | 39 | template 40 | struct unique { 41 | using type = T_tuple; 42 | }; 43 | 44 | template 45 | struct unique, T, T_rest...> { 46 | using type = std::conditional_t< 47 | std::disjunction_v...>, 48 | typename unique, T_rest...>::type, 49 | typename unique, T_rest...>::type 50 | >; 51 | }; 52 | 53 | template 54 | struct unique, std::tuple> : 55 | unique, T_values...> { }; 56 | 57 | template 58 | using unique_t = typename unique, std::tuple>::type; 59 | 60 | } /* namespace utils::type_traits */ 61 | 62 | #endif /* UTILS_TYPE_TRAITS_HPP */ -------------------------------------------------------------------------------- /juro/include/juro/compose/race.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file juro/compose/race.hpp 3 | * @brief Contains definitions of promise races and auxiliary structures 4 | * @author André Medeiros 5 | */ 6 | 7 | #ifndef JURO_COMPOSE_RACE_HPP 8 | #define JURO_COMPOSE_RACE_HPP 9 | 10 | #include "juro/helpers.hpp" 11 | #include "juro/factories.hpp" 12 | 13 | namespace juro::compose { 14 | 15 | using namespace juro::helpers; 16 | using namespace juro::factories; 17 | 18 | /** 19 | * @brief Base template for race result types; see concrete implementations for 20 | * more details 21 | * @tparam ... 22 | */ 23 | template 24 | struct race_result; 25 | 26 | template 27 | struct race_result> { 28 | using type = std::variant...>; 29 | }; 30 | 31 | template 32 | struct race_result> { 33 | using type = storage_type; 34 | }; 35 | 36 | template 37 | using race_result_t = typename race_result::type; 38 | 39 | template 40 | auto race(promise_ptr ...promises) { 41 | return make_promise>>([&] (auto &race_promise) { 42 | ([&] (auto &promise) { 43 | if constexpr(std::remove_reference_t::element_type::is_void) { 44 | promise->then( 45 | [=] { 46 | if(race_promise->is_pending()) { 47 | race_promise->resolve(void_type { }); 48 | } 49 | }, 50 | [=] (auto &error) { 51 | if(race_promise->is_pending()) { 52 | race_promise->reject(error); 53 | } 54 | } 55 | ); 56 | } else { 57 | promise->then( 58 | [=] (auto &value) { 59 | if(race_promise->is_pending()) { 60 | race_promise->resolve(std::move(value)); 61 | } 62 | }, 63 | [=] (auto &error) { 64 | if(race_promise->is_pending()) { 65 | race_promise->reject(error); 66 | } 67 | } 68 | ); 69 | } 70 | }(promises), ...); 71 | }); 72 | } 73 | 74 | } /* namespace juro::compose */ 75 | 76 | #endif /* JURO_COMPOSE_RACE_HPP */ -------------------------------------------------------------------------------- /utils/include/utils/logger.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_LOGGER_HPP 2 | #define UTILS_LOGGER_HPP 3 | 4 | #include 5 | #include 6 | #include "types.hpp" 7 | 8 | #ifdef NEBUG 9 | #define LOGGER_MOCK 1 10 | #else /* NDEBUG */ 11 | #define LOGGER_MOCK 0 12 | #endif /* NDEBUG */ 13 | 14 | namespace utils { 15 | 16 | using namespace types; 17 | 18 | class logger_scope { 19 | public: 20 | enum class entry_level { 21 | DEBUG = 0, 22 | INFO, 23 | WARN, 24 | ERROR, 25 | FATAL 26 | }; 27 | 28 | private: 29 | const char * const module; 30 | volatile u32 &timer; 31 | entry_level current_level; 32 | 33 | constexpr static const char * entry_tags[] = { 34 | "DEBUG", 35 | "INFO ", 36 | "WARN ", 37 | "ERROR", 38 | "FATAL" 39 | }; 40 | 41 | 42 | void log(entry_level level, const char *format, va_list &args) { 43 | if(LOGGER_MOCK || level < current_level) return; 44 | 45 | char buffer[1024]; 46 | int pos = sprintf(buffer, "[%08u][%s][%-12s] ", timer, entry_tags[static_cast(level)], module); 47 | if(pos > 0) { 48 | vsprintf(&buffer[pos], format, args); 49 | puts(buffer); 50 | } 51 | } 52 | 53 | public: 54 | 55 | inline logger_scope(const char *module, volatile u32 &timer, entry_level level = entry_level::INFO) : 56 | module(module), timer(timer), current_level(level) { } 57 | 58 | inline void set_level(entry_level level) { current_level = level; } 59 | 60 | void debug(const char *format, ...) { 61 | va_list args; 62 | va_start(args, format); 63 | log(entry_level::DEBUG, format, args); 64 | } 65 | 66 | void info(const char *format, ...) { 67 | va_list args; 68 | va_start(args, format); 69 | log(entry_level::INFO, format, args); 70 | } 71 | 72 | void warn(const char *format, ...) { 73 | va_list args; 74 | va_start(args, format); 75 | log(entry_level::WARN, format, args); 76 | } 77 | 78 | void error(const char *format, ...) { 79 | va_list args; 80 | va_start(args, format); 81 | log(entry_level::ERROR, format, args); 82 | } 83 | 84 | void fatal(const char *format, ...) { 85 | va_list args; 86 | va_start(args, format); 87 | log(entry_level::FATAL, format, args); 88 | } 89 | 90 | }; 91 | 92 | }; /* namespace utils */ 93 | 94 | #endif /* UTILS_LOGGER_HPP */ -------------------------------------------------------------------------------- /plumbing/include/plumbing/sink.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PLUMBING_SINK_HPP 2 | #define PLUMBING_SINK_HPP 3 | 4 | #include 5 | #include "plumbing/source.hpp" 6 | #include 7 | 8 | namespace plumbing { 9 | 10 | namespace messages { 11 | 12 | namespace active_sink { 13 | struct request_data : public fuss::message { }; 14 | } /* namespace active_sink */ 15 | } /* namespace messages */ 16 | 17 | template 18 | class sink { 19 | fuss::message_guard guard; 20 | 21 | public: 22 | using type_in = T; 23 | 24 | virtual ~sink() = default; 25 | virtual void consume([[maybe_unused]] const T &data) { } 26 | 27 | template()))> 28 | void consume(T_collection &&data) { 29 | for(auto &&datum : data) { 30 | consume(std::forward(datum)); 31 | } 32 | } 33 | 34 | template 35 | T_source &operator<<(T_source &source) { 36 | pipe_from(source); 37 | return source; 38 | } 39 | 40 | void virtual piped([[maybe_unused]] source &source) { } 41 | 42 | void pipe_from(source &source) { 43 | guard = source.template listen>([this] (T data) { 44 | consume(std::move(data)); 45 | }); 46 | piped(source); 47 | } 48 | 49 | }; 50 | 51 | template 52 | class active_sink : 53 | public sink, 54 | public fuss::shouter { 55 | 56 | public: 57 | void request_data(std::size_t count) { 58 | this->shout(count); 59 | } 60 | }; 61 | 62 | template 63 | class buffered_sink : public virtual sink{ 64 | circular_queue queue; 65 | std::size_t count; 66 | 67 | public: 68 | using sink::consume; 69 | 70 | void consume(const T &data) final { 71 | if(this->count > 0 && this->queue.is_empty()) { 72 | this->count--; 73 | this->requested_data(data); 74 | } else { 75 | this->queue.push(data); 76 | } 77 | } 78 | 79 | protected: 80 | virtual void requested_data(const T &data) = 0; 81 | 82 | void next(std::size_t count) { 83 | for(this->count = count; this->count > 0; this->count--) { 84 | if(this->queue.is_empty()) break; 85 | 86 | const T &data = this->queue.pop(); 87 | this->requested_data(data); 88 | } 89 | } 90 | }; 91 | 92 | } /* namespace plumbing*/ 93 | 94 | #endif /* PLUMBING_SINK_HPP */ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 35 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 36 | 37 | # User-specific stuff 38 | .idea/**/workspace.xml 39 | .idea/**/tasks.xml 40 | .idea/**/usage.statistics.xml 41 | .idea/**/dictionaries 42 | .idea/**/shelf 43 | 44 | # AWS User-specific 45 | .idea/**/aws.xml 46 | 47 | # Generated files 48 | .idea/**/contentModel.xml 49 | 50 | # Sensitive or high-churn files 51 | .idea/**/dataSources/ 52 | .idea/**/dataSources.ids 53 | .idea/**/dataSources.local.xml 54 | .idea/**/sqlDataSources.xml 55 | .idea/**/dynamic.xml 56 | .idea/**/uiDesigner.xml 57 | .idea/**/dbnavigator.xml 58 | 59 | # Gradle 60 | .idea/**/gradle.xml 61 | .idea/**/libraries 62 | 63 | # Gradle and Maven with auto-import 64 | # When using Gradle or Maven with auto-import, you should exclude module files, 65 | # since they will be recreated, and may cause churn. Uncomment if using 66 | # auto-import. 67 | # .idea/artifacts 68 | # .idea/compiler.xml 69 | # .idea/jarRepositories.xml 70 | # .idea/modules.xml 71 | # .idea/*.iml 72 | # .idea/modules 73 | # *.iml 74 | # *.ipr 75 | 76 | # CMake 77 | cmake-build-*/ 78 | 79 | # Mongo Explorer plugin 80 | .idea/**/mongoSettings.xml 81 | 82 | # File-based project format 83 | *.iws 84 | 85 | # IntelliJ 86 | out/ 87 | 88 | # mpeltonen/sbt-idea plugin 89 | .idea_modules/ 90 | 91 | # JIRA plugin 92 | atlassian-ide-plugin.xml 93 | 94 | # Cursive Clojure plugin 95 | .idea/replstate.xml 96 | 97 | # SonarLint plugin 98 | .idea/sonarlint/ 99 | 100 | # Crashlytics plugin (for Android Studio and IntelliJ) 101 | com_crashlytics_export_strings.xml 102 | crashlytics.properties 103 | crashlytics-build.properties 104 | fabric.properties 105 | 106 | # Editor-based Rest Client 107 | .idea/httpRequests 108 | 109 | # Android studio 3.1+ serialized cache file 110 | .idea/caches/build_file_checksums.ser 111 | 112 | dist 113 | */**/docs 114 | 115 | # Ignore header files in config directory; they are 116 | # automatically generated by CMake based on the .in 117 | # template file in the same directory 118 | config/**/*.hpp -------------------------------------------------------------------------------- /utils/include/utils/circular-queue.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_CIRCULAR_QUEUE_HPP 2 | #define UTILS_CIRCULAR_QUEUE_HPP 3 | 4 | #include 5 | #include "storage-for.hpp" 6 | 7 | namespace utils { 8 | 9 | template 10 | class circular_queue { 11 | using placeholder = storage_for; 12 | 13 | std::size_t head = 0, count = 0, capacity, mask; 14 | std::unique_ptr queue; 15 | 16 | inline std::size_t pos(std::size_t i, std::size_t mask) const noexcept { 17 | return i & mask; 18 | } 19 | 20 | inline std::size_t pos(std::size_t i) const noexcept { 21 | return pos(i, mask); 22 | } 23 | 24 | void grow() { 25 | std::size_t new_capacity = capacity * 2; 26 | auto new_queue = std::make_unique(new_capacity); 27 | 28 | std::size_t new_mask = new_capacity - 1; 29 | for(auto i = head; i != head + count; i++) { 30 | new_queue[pos(i, new_mask)].construct(queue[pos(i)].extract()); 31 | } 32 | 33 | capacity = new_capacity; 34 | mask = new_mask; 35 | queue = std::move(new_queue); 36 | } 37 | 38 | public: 39 | circular_queue(std::size_t factor_log2n = 3) : 40 | capacity { 1UL<(capacity) } 43 | { } 44 | 45 | ~circular_queue() { 46 | for(auto i = head; i < head + count; i++) { 47 | queue[pos(i)].destruct(); 48 | } 49 | }; 50 | 51 | circular_queue(circular_queue &&) = default; 52 | circular_queue(const circular_queue &) = delete; 53 | 54 | circular_queue &operator=(circular_queue &&) = 55 | default; 56 | circular_queue &operator=(const circular_queue &) = 57 | delete; 58 | 59 | inline std::size_t get_capacity() const noexcept { return capacity; } 60 | inline std::size_t get_count() const noexcept { return count; } 61 | inline bool is_empty() const noexcept { return count == 0; } 62 | 63 | void push(T_object object) { 64 | emplace(std::move(object)); 65 | } 66 | 67 | void emplace(auto && ...args) { 68 | if(count == capacity) grow(); 69 | queue[pos(head + count++)] 70 | .construct(std::forward(args)...); 71 | } 72 | 73 | T_object pop() { 74 | T_object object { queue[pos(head++)].extract() }; 75 | count--; 76 | return object; 77 | } 78 | 79 | void swap(circular_queue &other) noexcept { 80 | auto temp = std::move(other); 81 | other = std::move(*this); 82 | *this = std::move(temp); 83 | } 84 | }; 85 | 86 | } /* namespace utils */ 87 | 88 | #endif /* UTILS_CIRCULAR_QUEUE_HPP */ -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(iara LANGUAGES CXX VERSION 0.0.1) 3 | 4 | Include(FetchContent) 5 | 6 | if(NOT CMAKE_BUILD_TYPE) 7 | set(CMAKE_BUILD_TYPE Release) 8 | endif() 9 | 10 | set(CMAKE_CXX_STANDARD 17) 11 | set(CMAKE_CXX_STANDARD_REQUIRED True) 12 | set(CMAKE_VERBOSE_MAKEFILE ON) 13 | 14 | if (MSVC) 15 | add_compile_options(/W4) 16 | else() 17 | add_compile_options(-Wall -Wextra -Wpedantic -Werror) 18 | endif() 19 | 20 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/dist/bin) 21 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/dist/lib) 22 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/dist/lib) 23 | 24 | 25 | # Public libraries 26 | 27 | # Config 28 | add_library(config INTERFACE) 29 | target_include_directories(config INTERFACE config/include) 30 | 31 | # Fugax 32 | set(fugax_source_files 33 | fugax/src/event.cpp 34 | fugax/src/event-loop.cpp 35 | fugax/src/event-guard.cpp 36 | ) 37 | add_library(fugax ${fugax_source_files}) 38 | target_include_directories(fugax PUBLIC fugax/include) 39 | target_link_libraries(fugax PUBLIC config juro iara-utils) 40 | configure_file( 41 | ${PROJECT_SOURCE_DIR}/config/include/config/fugax.hpp.in 42 | ${PROJECT_SOURCE_DIR}/config/include/config/fugax.hpp 43 | @ONLY 44 | ) 45 | 46 | # Fuss 47 | add_library(fuss INTERFACE) 48 | target_include_directories(fuss INTERFACE fuss/include) 49 | 50 | # Juro 51 | set(juro_source_files juro/src/promise.cpp juro/src/compose/all.cpp) 52 | add_library(juro ${juro_source_files}) 53 | target_include_directories(juro PUBLIC juro/include utils/include) 54 | 55 | # Plumbing 56 | add_library(plumbing INTERFACE) 57 | target_link_libraries(plumbing INTERFACE iara-utils fuss) 58 | target_include_directories(plumbing INTERFACE plumbing/include) 59 | 60 | # Iara 61 | add_library(iara ${juro_source_files} ${fugax_source_files}) 62 | target_include_directories(iara PUBLIC config/include fuss/include fugax/include juro/include plumbing/include utils/include) 63 | 64 | # Utils 65 | add_library(iara-utils INTERFACE) 66 | target_include_directories(iara-utils INTERFACE utils/include) 67 | 68 | # Tests 69 | 70 | # Tests are written with Catch2 71 | FetchContent_Declare( 72 | Catch2 73 | GIT_REPOSITORY https://github.com/catchorg/Catch2.git 74 | GIT_TAG v3.3.2 75 | ) 76 | FetchContent_MakeAvailable(Catch2) 77 | 78 | # Fugax tests 79 | set(fugax_test_source_files test/src/fugax/test.cpp) 80 | 81 | # Fuss tests 82 | set(fuss_test_source_files test/src/fuss/test.cpp) 83 | 84 | # Juro tests 85 | set(juro_test_source_files test/src/juro/test.cpp) 86 | 87 | add_executable(iara-test 88 | ${fugax_test_source_files} 89 | ${fuss_test_source_files} 90 | ${juro_test_source_files} 91 | ) 92 | target_link_libraries(iara-test PRIVATE juro fuss fugax Catch2::Catch2WithMain) 93 | target_include_directories(iara-test PUBLIC test/include) 94 | 95 | list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) 96 | include(CTest) 97 | include(Catch) 98 | catch_discover_tests(iara-test) -------------------------------------------------------------------------------- /juro/include/juro/factories.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file juro/factories.hpp 3 | * @brief Contains factory functions for creating promises in all possible 4 | * states. 5 | * @author André Medeiros 6 | */ 7 | 8 | #ifndef JURO_FACTORIES_HPP 9 | #define JURO_FACTORIES_HPP 10 | 11 | #include "juro/helpers.hpp" 12 | 13 | namespace juro::factories { 14 | 15 | using namespace juro::helpers; 16 | 17 | /** 18 | * @brief Creates a new promise and supplies it to the provided launcher 19 | * functor. This promise can than be dispatched elsewhere. 20 | * @tparam T The type of the promise being created 21 | * @tparam T_launcher The type of the launched functor 22 | * @param launcher The launched functor 23 | * @return The newly created promise 24 | */ 25 | template 26 | auto make_promise(T_launcher &&launcher) { 27 | static_assert( 28 | std::is_invocable_v &>, 29 | "Launcher function has an incompatible signature." 30 | ); 31 | const auto p = std::make_shared>(); 32 | launcher(p); 33 | return p; 34 | } 35 | 36 | /** 37 | * @brief Creates a new pending promise. 38 | * @tparam T The type of the promis ebeing created 39 | * @return The newly created promise 40 | */ 41 | template 42 | auto make_pending() { 43 | return std::make_shared>(); 44 | } 45 | 46 | /** 47 | * @brief Creates a new non-void resolved promise. 48 | * @tparam T The type of the promise being created. Unless explicitly supplied, 49 | * will be inferred from the `value` parameter. 50 | * @param value The value with which to resolve the promise 51 | * @return The newly created promise 52 | */ 53 | template 54 | auto make_resolved(T &&value) { 55 | return std::make_shared>>( 56 | resolved_promise_tag { }, 57 | std::forward(value) 58 | ); 59 | } 60 | 61 | /** 62 | * @brief Creates a new void resolved promise. 63 | * @return The newly created promise 64 | */ 65 | inline auto make_resolved() { 66 | return std::make_shared>( 67 | resolved_promise_tag { }, 68 | void_type { } 69 | ); 70 | } 71 | 72 | /** 73 | * @brief Creates a new rejected promise. 74 | * @note This is the only supported way of creating a rejected promise without 75 | * an attached settle handler. Creating a pending promise and immediately 76 | * rejecting it would throw a `promise_error` with message 77 | * "Unhandled promise rejection". 78 | * @tparam T The type of the promise being created. If unsupplied, defaults to 79 | * `void` 80 | * @tparam T_value The type of the value with which to reject the promise 81 | * @param value The value with which to reject the promise 82 | * @return The newly create promise 83 | */ 84 | template 85 | auto make_rejected(T_value &&value = T_value { "Promise was rejected" }) { 86 | return std::make_shared>>( 87 | rejected_promise_tag { }, 88 | std::forward(value) 89 | ); 90 | } 91 | 92 | } /* namespace juro::factories */ 93 | 94 | #endif /* JURO_FACTORIES_HPP */ -------------------------------------------------------------------------------- /plumbing/include/plumbing/box.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PLUMBING_BOX_HPP 2 | #define PLUMBING_BOX_HPP 3 | 4 | #include "source.hpp" 5 | #include "sink.hpp" 6 | #include 7 | 8 | 9 | namespace plumbing { 10 | namespace box { 11 | namespace { 12 | 13 | template 14 | T_last *pipe_many(T_first *first, T_last *last) { 15 | first->pipe_to(*last); 16 | return last; 17 | } 18 | 19 | template 20 | T_last *pipe_many(T_first *first, T_next *next, T_rest * ... rest, T_last *last) { 21 | first->pipe_to(*next); 22 | return pipe_many(next, rest ..., last); 23 | } 24 | 25 | template 26 | class box { 27 | protected: 28 | std::tuple segments; 29 | 30 | public: 31 | virtual ~box() = default; 32 | box(T_args * ... args) : segments(args ...) { 33 | pipe_many(args ...); 34 | } 35 | }; 36 | 37 | template 38 | class first { 39 | using tuple = std::tuple; 40 | public: 41 | using type = typename std::tuple_element<0, tuple>::type::type_in; 42 | }; 43 | 44 | template 45 | class last { 46 | using tuple = std::tuple; 47 | using pos = std::tuple_size; 48 | public: 49 | using type = typename std::tuple_element::type::type_out; 50 | }; 51 | 52 | template 53 | class source : 54 | public stream::source::type>, 55 | public virtual box { 56 | 57 | public: 58 | source(T_args * ... segments) : box(segments ...) { } 59 | 60 | protected: 61 | using source_type = typename last::type; 62 | 63 | void pipe_to(sink &target) final { 64 | auto constexpr last_pos = 65 | std::tuple_size>::value - 1; 66 | *std::get(this->segments) >> target; 67 | } 68 | }; 69 | 70 | template 71 | class sink : 72 | public stream::sink::type>, 73 | public virtual box { 74 | 75 | void consume(const typename first::type &data) final { 76 | std::get<0>(this->segments)->consume(data); 77 | } 78 | 79 | public: 80 | sink(T_args * ... segments) : box(segments ...) { } 81 | 82 | 83 | }; 84 | 85 | template 86 | class duplex : 87 | public stream::box::sink, 88 | public stream::box::source { 89 | 90 | duplex(T_args * ... segments) : 91 | stream::box::source(segments ...) { } 92 | }; 93 | } /* anonymous namespace */ 94 | 95 | template 96 | source *make_source(T_args * ... args) { 97 | return new source(args ...); 98 | } 99 | 100 | 101 | } /* namespace box */ 102 | 103 | } /* namespace plumbing */ 104 | 105 | 106 | #endif /* PLUMBING_BOX_HPP */ -------------------------------------------------------------------------------- /fugax/include/fugax/event-guard.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file fugax/include/fugax/event-guard.hpp 3 | * @brief Contains definition of event guards, a RAII-enabled container for 4 | * event listeners 5 | * @author André Medeiros 6 | * @date 22/06/23 7 | * @copyright 2023 (C) André Medeiros 8 | **/ 9 | 10 | #ifndef IARA_EVENT_GUARD_HPP 11 | #define IARA_EVENT_GUARD_HPP 12 | 13 | #include "event-listener.hpp" 14 | 15 | namespace fugax { 16 | 17 | /** 18 | * @brief An event guard is a RAII-style container for an event listener. 19 | * It has a converting constructor and can be directly initialised from 20 | * an event listener. Upon destruction, it will attempt to cancel the 21 | * event 22 | */ 23 | class event_guard { 24 | /** 25 | * @brief The listener (i.e. a weak pointer) of the event; upon 26 | * destruction, the guard will attempt to release its ownership 27 | * over this 28 | * @see `fugax::event_guard::release` 29 | */ 30 | event_listener listener; 31 | 32 | public: 33 | 34 | /** 35 | * @brief Default constructor; yields an empty event guard 36 | */ 37 | inline constexpr event_guard() noexcept = default; 38 | 39 | /** 40 | * @brief Move constructor; empties the other guard and assumes 41 | * ownership of its listener 42 | */ 43 | inline event_guard(event_guard &&) noexcept = default; 44 | 45 | /** 46 | * @brief Copy constructor is deleted; event guards are move-only 47 | */ 48 | event_guard(const event_guard &) = delete; 49 | 50 | /** 51 | * @brief Converting constructor for event listeners 52 | * @param listener The event listener to store in the guard 53 | */ 54 | inline event_guard(event_listener &&listener) noexcept : 55 | listener { std::move(listener) } 56 | { } 57 | 58 | /** 59 | * @brief Converting constructor for event listeners 60 | * @param listener The event listener to store in the guard 61 | */ 62 | inline event_guard(const event_listener &listener) noexcept : 63 | listener { listener } 64 | { } 65 | 66 | /** 67 | * @brief Upon destruction, the guard attempts to release its ownership 68 | * by calling `.release()` 69 | * @see `fugax::event_guard::release` 70 | */ 71 | inline ~event_guard() noexcept { release(); } 72 | 73 | /** 74 | * @brief Move-assignment operator; reassigns this guard by providing it 75 | * another guard's listener; if this guard owns a listener already, it will 76 | * attempt to release it first 77 | * @param other Another event guard (that might have been construct via 78 | * conversion construction) whose listener will be moved into this guard; 79 | * after assignment, it will be empty 80 | * @return A reference to `this` 81 | */ 82 | event_guard &operator=(event_guard &&other) noexcept; 83 | 84 | /** 85 | * @brief Copy-assignment is deleted because `event_guard` is a move-only 86 | * type 87 | */ 88 | event_guard &operator=(const event_guard &other) = delete; 89 | 90 | /** 91 | * @brief Attempts to release the holden listener by acquiring its 92 | * shared pointer, and, if succeeded, cancels the event; if locking 93 | * the listener fails, does nothing 94 | */ 95 | void release() const noexcept; 96 | 97 | /** 98 | * @brief Gets a const reference to the contained event listener 99 | * @return A read-only reference to the contained listener 100 | */ 101 | inline constexpr event_listener const &get() const noexcept { return listener; } 102 | }; 103 | 104 | } /* namespace fugax */ 105 | 106 | #endif /* IARA_EVENT_GUARD_HPP */ 107 | -------------------------------------------------------------------------------- /juro/include/juro/compose/all.hpp: -------------------------------------------------------------------------------- 1 | #ifndef JURO_COMPOSE_ALL_HPP 2 | #define JURO_COMPOSE_ALL_HPP 3 | 4 | #include 5 | #include 6 | #include "juro/helpers.hpp" 7 | #include "juro/factories.hpp" 8 | 9 | namespace juro::compose { 10 | 11 | using namespace juro::helpers; 12 | using namespace juro::factories; 13 | 14 | template 15 | using all_result = std::tuple...>; 16 | 17 | template 18 | class all_coordinator : public std::enable_shared_from_this> { 19 | using result_type = all_result; 20 | using transient_type = std::tuple>...>; 21 | 22 | transient_type working_area; 23 | std::size_t counter = sizeof...(T_values); 24 | promise_ptr promise; 25 | 26 | public: 27 | all_coordinator(const promise_ptr &promise) : 28 | promise { promise } 29 | { } 30 | 31 | template 32 | void attach(std::index_sequence, std::tuple &...> &&promises) { 33 | ([&, this] (auto &promise, auto &slot) { 34 | using promise_type = 35 | typename std::remove_reference_t::element_type; 36 | if constexpr(promise_type::is_void) { 37 | promise->then( 38 | [this, &slot, guard = this->shared_from_this()] { 39 | on_resolve(void_type {}, slot); 40 | }, 41 | [&] (std::exception_ptr &error) { on_reject(error); } 42 | ); 43 | } else { 44 | promise->then( 45 | [this, &slot, guard = this->shared_from_this()] (auto &value) { 46 | on_resolve(value, slot); 47 | }, 48 | [&] (std::exception_ptr &error) { on_reject(error); } 49 | ); 50 | } 51 | }(std::get(promises), std::get(working_area)), ...); 52 | } 53 | 54 | template 55 | void on_resolve(T_value &&value, std::optional> &slot) { 56 | slot = std::forward(value); 57 | 58 | if(--counter == 0 && promise->is_pending()) { 59 | promise->resolve(std::apply([] (auto &...values) { 60 | return std::make_tuple(std::move(values.value())...); 61 | }, working_area)); 62 | } 63 | } 64 | 65 | void on_reject(std::exception_ptr &error) { 66 | if(promise->is_pending()) { 67 | promise->reject(error); 68 | } 69 | } 70 | }; 71 | 72 | void void_settler(const promise_ptr &promise, const promise_ptr &all_promise, const std::shared_ptr &counter); 73 | 74 | template 75 | auto all(const promise_ptr &...promises) { 76 | if constexpr(std::conjunction_v...>) { 77 | return make_promise([&] (const auto &all_promise) { 78 | auto counter = std::make_shared(sizeof...(T_values)); 79 | for(const auto &promise : std::array>, sizeof...(T_values)> { promises... }) { 80 | void_settler(promise, all_promise, counter); 81 | } 82 | }); 83 | } else { 84 | return make_promise>([&] (const auto &all_promise) { 85 | auto coordinator = std::make_shared>(all_promise); 86 | coordinator->attach( 87 | std::index_sequence_for(), 88 | std::forward_as_tuple(promises...) 89 | ); 90 | }); 91 | } 92 | } 93 | 94 | } /* namespace juro::compose */ 95 | 96 | #endif /* JURO_COMPOSE_ALL_HPP */ -------------------------------------------------------------------------------- /fugax/src/event-loop.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file fugax/src/event-loop.cpp 3 | * @brief Implementation of non-templated event loop functions 4 | * @author André Medeiros 5 | * @date 26/06/23 6 | * @copyright 2023 (C) André Medeiros 7 | */ 8 | 9 | #include "fugax/event-loop.hpp" 10 | 11 | namespace fugax { 12 | 13 | event_listener event_loop::schedule(event_handler functor) { 14 | return schedule(0, schedule_policy::immediate, std::move(functor)); 15 | } 16 | 17 | event_listener event_loop::schedule(time_type delay, event_handler functor) { 18 | return schedule(delay, schedule_policy::delayed, std::move(functor)); 19 | } 20 | 21 | event_listener event_loop::schedule(time_type delay, bool recurring, event_handler functor) { 22 | auto policy = recurring ? 23 | schedule_policy::recurring_delayed : 24 | schedule_policy::delayed; 25 | return schedule(delay, policy, std::move(functor)); 26 | } 27 | 28 | event_listener event_loop::schedule(time_type delay, schedule_policy policy, event_handler functor) { 29 | std::lock_guard _ { mutex }; 30 | 31 | time_type due_time, interval; 32 | bool recurring; 33 | 34 | switch(policy) { 35 | case schedule_policy::immediate: 36 | std::tie(due_time, recurring, interval) = std::tuple { counter, false, 0 }; 37 | break; 38 | case schedule_policy::delayed: 39 | std::tie(due_time, recurring, interval) = std::tuple { counter + delay, false, 0 }; 40 | break; 41 | case schedule_policy::recurring_immediate: 42 | std::tie(due_time, recurring, interval) = std::tuple { counter, true, delay }; 43 | break; 44 | case schedule_policy::recurring_delayed: 45 | std::tie(due_time, recurring, interval) = std::tuple { counter + delay, true, delay }; 46 | break; 47 | case schedule_policy::always: 48 | std::tie(due_time, recurring, interval) = std::tuple { counter, true, 0 }; 49 | break; 50 | default: 51 | return { }; 52 | } 53 | 54 | return timers[due_time].emplace_back( 55 | std::make_shared(std::move(functor), interval, due_time, recurring) 56 | ); 57 | } 58 | 59 | event_listener event_loop::always(event_handler functor) { 60 | return schedule(0, schedule_policy::always, std::move(functor)); 61 | } 62 | 63 | void event_loop::process(time_type now) { 64 | auto queue = get_due_timers(now); 65 | 66 | auto entry = queue.begin(); 67 | while(entry != queue.end()) { 68 | const auto removing = entry++; 69 | const auto &event = *removing; 70 | 71 | if(event->cancelled) continue; 72 | 73 | if(event->due_time <= now) { // Event is due to be fired 74 | event->fire(); 75 | 76 | if(event->recurring) { 77 | std::lock_guard _ { mutex }; 78 | 79 | auto due_time = now + event->interval; 80 | auto &target = timers[due_time]; 81 | 82 | event->due_time = due_time; 83 | target.splice(target.end(), queue, removing); 84 | } 85 | } 86 | else { // Event has been rescheduled 87 | std::lock_guard _ { mutex }; 88 | auto &target = timers[event->due_time]; 89 | target.splice(target.end(), queue, removing); 90 | } 91 | } 92 | 93 | counter = now; 94 | } 95 | 96 | juro::promise_ptr event_loop::wait(time_type delay) { 97 | return juro::make_promise([&] (const auto &promise) { 98 | schedule(delay, [=] { promise->resolve(); }); 99 | }); 100 | } 101 | 102 | 103 | event_loop::event_queue event_loop::get_due_timers(time_type now) noexcept { 104 | event_queue queue; 105 | std::lock_guard _ { mutex }; 106 | 107 | auto entry = timers.begin(); 108 | while(entry != timers.end()) { 109 | const auto removing = entry++; 110 | auto & [ time_point, events ] = *removing; 111 | if(time_point > now) break; 112 | 113 | queue.splice(queue.end(), events); 114 | if(time_point != now) { 115 | timers.erase(removing); 116 | } 117 | } 118 | 119 | return queue; 120 | } 121 | 122 | } /* namespace fugax */ 123 | -------------------------------------------------------------------------------- /utils/include/utils/test-helpers.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file utils/include/utils/test-helpers.hpp 3 | * @brief Test helpers and utilities 4 | * @author André Medeiros 5 | * @date 27/06/23 6 | * @copyright 2023 (C) André Medeiros 7 | **/ 8 | 9 | #ifndef UTILS_TEST_HELPERS_HPP 10 | #define UTILS_TEST_HELPERS_HPP 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | namespace utils::test_helpers { 17 | 18 | template 19 | class safe_result { 20 | using stored_type = std::variant; 21 | stored_type value; 22 | 23 | public: 24 | using value_type = T; 25 | 26 | constexpr explicit safe_result(T &&value = { }) 27 | noexcept(std::is_nothrow_constructible_v) : 28 | value { std::move(value) } 29 | { } 30 | 31 | constexpr explicit safe_result(const T &value = { }) 32 | noexcept(std::is_nothrow_constructible_v) : 33 | value { value } 34 | { } 35 | 36 | constexpr explicit safe_result(std::exception_ptr &&error) 37 | noexcept(std::is_nothrow_constructible_v) : 38 | value { std::move(error) } 39 | { } 40 | 41 | constexpr explicit safe_result(const std::exception_ptr &&error) 42 | noexcept(std::is_nothrow_constructible_v) : 43 | value { error } 44 | { } 45 | 46 | template 47 | constexpr safe_result(std::in_place_t, T_args &&...args) 48 | noexcept(std::is_nothrow_constructible_v) : 49 | value { std::in_place_type_t(), { std::forward(args)... } } 50 | { } 51 | 52 | constexpr bool has_value() const noexcept { 53 | return std::holds_alternative(value); 54 | } 55 | 56 | constexpr bool has_error() const noexcept { 57 | return std::holds_alternative(value); 58 | } 59 | 60 | constexpr T &get_value() { 61 | return std::get(value); 62 | } 63 | 64 | constexpr const T &get_value() const { 65 | return std::get(value); 66 | } 67 | 68 | template 69 | T_error &get_error() const try { 70 | std::rethrow_exception(std::get(value)); 71 | } 72 | catch(T_error &t) { return t; } 73 | 74 | template 75 | constexpr bool holds_value() const noexcept { 76 | return std::is_same_v && has_value(); 77 | } 78 | 79 | template 80 | bool holds_error() const noexcept try { 81 | if(has_value()) return false; 82 | 83 | std::rethrow_exception(std::get(value)); 84 | } 85 | catch(T_error &) { return true; } 86 | catch(...) { return false; } 87 | }; 88 | 89 | template<> 90 | class safe_result { 91 | std::optional error; 92 | 93 | public: 94 | using value_type = void; 95 | 96 | constexpr safe_result() noexcept = default; 97 | 98 | constexpr explicit safe_result(std::exception_ptr &&error) 99 | noexcept(std::is_nothrow_constructible_v< 100 | std::optional, 101 | std::exception_ptr && 102 | >) : 103 | error { std::move(error) } 104 | { } 105 | 106 | constexpr explicit safe_result(const std::exception_ptr &error) 107 | noexcept(std::is_nothrow_constructible_v< 108 | std::optional, 109 | const std::exception_ptr & 110 | >) : 111 | error { error } 112 | { } 113 | 114 | constexpr bool has_value() const noexcept { 115 | return !has_error(); 116 | } 117 | 118 | constexpr bool has_error() const noexcept { 119 | return error.has_value(); 120 | } 121 | 122 | template 123 | T_error &get_error() const try { 124 | std::rethrow_exception(error.value()); 125 | } 126 | catch(T_error &t) { return t; } 127 | 128 | template 129 | bool holds_error() const noexcept try { 130 | if(has_value()) return false; 131 | 132 | std::rethrow_exception(error.value()); 133 | } 134 | catch(T_error &) { return true; } 135 | catch(...) { return false; } 136 | }; 137 | 138 | template 139 | safe_result> attempt(T_task &&task) noexcept try { 140 | if constexpr(std::is_void_v>) { 141 | task(); 142 | return { }; 143 | } else { 144 | return safe_result> { task() }; 145 | } 146 | } 147 | catch(...) { return safe_result> { std::current_exception() }; } 148 | 149 | inline safe_result rescue(const std::exception_ptr &error) 150 | noexcept(std::is_nothrow_constructible_v, const std::exception_ptr &>) 151 | { 152 | return safe_result { error }; 153 | } 154 | 155 | } /* namespace utils::test_helpers */ 156 | 157 | #endif /* UTILS_TEST_HELPERS_HPP */ 158 | -------------------------------------------------------------------------------- /utils/include/utils/types.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_TYPES_HPP 2 | #define UTILS_TYPES_HPP 3 | 4 | #include 5 | 6 | namespace utils::types { 7 | 8 | using u8 = std::uint8_t; 9 | using u16 = std::uint16_t; 10 | using u32 = std::uint32_t; 11 | using u64 = std::uint64_t; 12 | 13 | using u8_least = std::uint_least8_t; 14 | using u16_least = std::uint_least16_t; 15 | using u32_least = std::uint_least32_t; 16 | using u64_least = std::uint_least64_t; 17 | 18 | using u8_fast = std::uint_fast8_t; 19 | using u16_fast = std::uint_fast16_t; 20 | using u32_fast = std::uint_fast32_t; 21 | using u64_fast = std::uint_fast64_t; 22 | 23 | using u_max = std::uintmax_t; 24 | using u_ptr = std::uintptr_t; 25 | 26 | using i8 = std::int8_t; 27 | using i16 = std::int16_t; 28 | using i32 = std::int32_t; 29 | using i64 = std::int64_t; 30 | 31 | using i8_least = std::int_least8_t; 32 | using i16_least = std::int_least16_t; 33 | using i32_least = std::int_least32_t; 34 | using i64_least = std::int_least64_t; 35 | 36 | using i8_fast = std::int_fast8_t; 37 | using i16_fast = std::int_fast16_t; 38 | using i32_fast = std::int_fast32_t; 39 | using i64_fast = std::int_fast64_t; 40 | 41 | using i_max = std::intmax_t; 42 | using i_ptr = std::intptr_t; 43 | 44 | constexpr static inline u8 operator "" _u8(unsigned long long value) { 45 | return static_cast(value); 46 | } 47 | constexpr static inline u16 operator "" _u16(unsigned long long value) { 48 | return static_cast(value); 49 | } 50 | constexpr static inline u32 operator "" _u32(unsigned long long value) { 51 | return static_cast(value); 52 | } 53 | constexpr static inline u64 operator "" _u64(unsigned long long value) { 54 | return static_cast(value); 55 | } 56 | 57 | constexpr static inline u8_least operator "" _u8_least(unsigned long long value) { 58 | return static_cast(value); 59 | } 60 | constexpr static inline u16_least operator "" _u16_least(unsigned long long value) { 61 | return static_cast(value); 62 | } 63 | constexpr static inline u32_least operator "" _u32_least(unsigned long long value) { 64 | return static_cast(value); 65 | } 66 | constexpr static inline u64_least operator "" _u64_least(unsigned long long value) { 67 | return static_cast(value); 68 | } 69 | 70 | constexpr static inline u8_fast operator "" _u8_fast(unsigned long long value) { 71 | return static_cast(value); 72 | } 73 | constexpr static inline u16_fast operator "" _u16_fast(unsigned long long value) { 74 | return static_cast(value); 75 | } 76 | constexpr static inline u32_fast operator "" _u32_fast(unsigned long long value) { 77 | return static_cast(value); 78 | } 79 | constexpr static inline u64_fast operator "" _u64_fast(unsigned long long value) { 80 | return static_cast(value); 81 | } 82 | 83 | constexpr static inline u_max operator "" _u_max(unsigned long long value) { 84 | return static_cast(value); 85 | } 86 | constexpr static inline u_ptr operator "" _u_ptr(unsigned long long value) { 87 | return static_cast(value); 88 | } 89 | 90 | constexpr static inline i8 operator "" _i8(unsigned long long value) { 91 | return static_cast(value); 92 | } 93 | constexpr static inline i16 operator "" _i16(unsigned long long value) { 94 | return static_cast(value); 95 | } 96 | constexpr static inline i32 operator "" _i32(unsigned long long value) { 97 | return static_cast(value); 98 | } 99 | constexpr static inline i64 operator "" _i64(unsigned long long value) { 100 | return static_cast(value); 101 | } 102 | 103 | constexpr static inline i8_least operator "" _i8_least(unsigned long long value) { 104 | return static_cast(value); 105 | } 106 | constexpr static inline i16_least operator "" _i16_least(unsigned long long value) { 107 | return static_cast(value); 108 | } 109 | constexpr static inline i32_least operator "" _i32_least(unsigned long long value) { 110 | return static_cast(value); 111 | } 112 | constexpr static inline i64_least operator "" _i64_least(unsigned long long value) { 113 | return static_cast(value); 114 | } 115 | 116 | constexpr static inline i8_fast operator "" _i8_fast(unsigned long long value) { 117 | return static_cast(value); 118 | } 119 | constexpr static inline i16_fast operator "" _i16_fast(unsigned long long value) { 120 | return static_cast(value); 121 | } 122 | constexpr static inline i32_fast operator "" _i32_fast(unsigned long long value) { 123 | return static_cast(value); 124 | } 125 | constexpr static inline i64_fast operator "" _i64_fast(unsigned long long value) { 126 | return static_cast(value); 127 | } 128 | 129 | constexpr static inline i_max operator "" _i_max(unsigned long long value) { 130 | return static_cast(value); 131 | } 132 | constexpr static inline i_ptr operator "" _i_ptr(unsigned long long value) { 133 | return static_cast(value); 134 | } 135 | 136 | } /* namespace utils::types */ 137 | 138 | #endif /* UTILS_TYPES_HPP */ -------------------------------------------------------------------------------- /fugax/include/fugax/event.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file fugax/include/fugax/event.hpp 3 | * @brief Definitions for events and auxiliary constructs 4 | * @author André Medeiros 5 | * @date 26/06/23 6 | * @copyright 2023 (C) André Medeiros 7 | */ 8 | 9 | #ifndef FUGAX_EVENT_HPP 10 | #define FUGAX_EVENT_HPP 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace fugax { 18 | using namespace config::fugax; 19 | 20 | class event; 21 | 22 | /** 23 | * @brief Basic invocable interface, used for type-erasing event handler functors 24 | */ 25 | class invocable_interface { 26 | public: 27 | /** 28 | * @brief Invoke action; should execute the wrapped functor 29 | */ 30 | virtual void invoke(event &) = 0; 31 | virtual ~invocable_interface() noexcept = default; 32 | }; 33 | 34 | /** 35 | * @brief A concrete implementation of the invocable interface that wraps 36 | * a determined type of functor 37 | * @tparam T_functor The type of the functor that is wrapped by this invocable 38 | */ 39 | template 40 | class invocable : public invocable_interface { 41 | static_assert( 42 | std::disjunction_v< 43 | std::is_invocable, 44 | std::is_invocable 45 | >, 46 | "An event handler functor must accept one event& parameter " \ 47 | "or no parameter at all." 48 | ); 49 | 50 | public: 51 | /** 52 | * @brief The functor object wrapped by this invocable; when the invocable's 53 | * `.invoke()` member function is called, this functor will be executed 54 | */ 55 | T_functor functor; 56 | 57 | /** 58 | * @brief Constructs a new invocable object from the supplied functor 59 | * @tparam T_func The type of the functor being wrapped 60 | * @param func The functor object to be stored inside the invocable 61 | */ 62 | template 63 | inline invocable(T_func &&func) noexcept : 64 | functor { std::forward(func) } 65 | { } 66 | 67 | /** 68 | * @brief Copy constructor is deleted; invocables are move-only 69 | */ 70 | invocable(const invocable &) = delete; 71 | 72 | /** 73 | * @brief Move constructor is defaulted and is noexcept if the wrapped 74 | * functor type is nothrow move constructible; 75 | */ 76 | invocable(invocable &&) 77 | noexcept(std::is_nothrow_move_constructible_v) = default; 78 | 79 | /** 80 | * @brief Invokes the wrapped functor, passing the associated event along if 81 | * it accepts parameters 82 | * @param ev The fired event that originated this invocable call 83 | */ 84 | virtual void invoke(event &ev) override { 85 | if constexpr(std::is_invocable_v) { 86 | functor(); 87 | } else { 88 | functor(ev); 89 | } 90 | } 91 | }; 92 | 93 | /** 94 | * @brief A type-erased container for event handlers 95 | */ 96 | class event_handler { 97 | /** 98 | * @brief The type-erased pointer to the invocable instance 99 | */ 100 | std::unique_ptr handler; 101 | 102 | public: 103 | /** 104 | * @brief Constructs a new event handler from a given functor 105 | * @tparam T_functor The type of the functor referenced by this event handler 106 | * @param functor The functor that gets executed when the handler is called 107 | */ 108 | template 109 | inline event_handler(T_functor &&functor) : 110 | handler { 111 | std::make_unique>(std::forward(functor)) 112 | } 113 | { } 114 | 115 | /** 116 | * @brief Calls this event handler 117 | * @param ev The fired event that originated this handler call 118 | */ 119 | void operator()(event &ev) const; 120 | }; 121 | 122 | class event_loop; 123 | 124 | /** 125 | * @brief An event represents an asynchronous task scheduled in the event loop 126 | */ 127 | class event { 128 | friend event_loop; 129 | 130 | /** 131 | * @brief The event handler to be called when this event's due time arrives 132 | */ 133 | const event_handler handler; 134 | 135 | /** 136 | * @brief For recurring events, this field stores how many time units must 137 | * pass between two successive activations of this event; ignored for 138 | * non-recurring events 139 | */ 140 | const time_type interval; 141 | 142 | /** 143 | * @brief The time value when execution is intended to occur; this gets updated 144 | * when an event is rescheduled, so a mismatch between the timer map key under 145 | * which this event is stored and this value indicates that the event must be 146 | * moved to a later slot instead of fired 147 | */ 148 | std::atomic due_time; 149 | 150 | /** 151 | * @brief A flag that indicates whether this event should be fired just once or 152 | * multiple times 153 | */ 154 | const bool recurring; 155 | 156 | /** 157 | * @brief A flag that indicates if an event has been cancelled, what will cause it 158 | * to not be fired anymore by the event loop and be destroyed when its due time 159 | * arrives 160 | */ 161 | std::atomic cancelled = false; 162 | 163 | /** 164 | * @brief Fires this event, invoking its corresponding handler 165 | */ 166 | void fire(); 167 | 168 | public: 169 | 170 | /** 171 | * @brief Constructs a new event 172 | * @param handler The handler to be called when this event is due 173 | * @param interval The interval between two successive activations of this event; 174 | * is ignored unless the `recurring` parameter is true 175 | * @param due_time This event's due time 176 | * @param recurring Whether this event is recurring or one-shot 177 | */ 178 | event(event_handler &&handler, time_type interval, time_type due_time, bool recurring); 179 | 180 | /** 181 | * @brief Cancels this event, preventing future execution; it will also cause the 182 | * event to be destroyed by the event loop eventually 183 | */ 184 | void cancel() noexcept; 185 | 186 | /** 187 | * @brief Reschedules this event to be run in a different time then what was initially 188 | * set; when the original time value compares equal to the counter value; the event 189 | * will be relocated around the timer map 190 | * @param time_point This event's new due time 191 | */ 192 | void reschedule(time_type time_point) noexcept; 193 | 194 | /** 195 | * @brief Returns whether this event has been cancelled or not 196 | * @return Whether the internal `cancelled` flag is set 197 | */ 198 | inline bool is_cancelled() const noexcept { return cancelled; } 199 | }; 200 | 201 | } /* namespace fugax */ 202 | 203 | #endif /* FUGAX_EVENT_HPP */ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Iara 2 | 3 | ![cmake build status](https://github.com/andsmedeiros/iara/actions/workflows/cmake.yml/badge.svg) 4 | 5 | Iara is a modular framework for composing asynchronous systems in C++17. It is distributed in 6 | form of several core libraries and some top-level glue code to stitch all together. 7 | 8 | Iara is portable, unopinionated, light, memory safe and flexible. It can be made to run in 9 | whatever gets a decent enough compiler support. 10 | 11 | ## Disclaimer from the creator 12 | 13 | Iara is a very WIP. It is born out of need for formalisation of some libraries I developed for 14 | internal use and that, even though had been successfully integrated into production environments 15 | for years, lacked proper testing, documentation and refactoring needed. 16 | 17 | As such, most of the available libraries are fully functional, very well tested and many of them 18 | are extremely well documented, both with source annotations and usage guides. However, there is 19 | still some work to be done in the libraries, as well as there are other custom libraries I would 20 | like to create or integrate to this project. 21 | 22 | Furthermore, I still envision a bunch of functionality to be implemented by a top-level 23 | application container under the `iara` namespace; this is still to be developed. 24 | 25 | ## Project libraries 26 | 27 | ### Fugax 28 | 29 | [Fugax](https://github.com/andsmedeiros/iara/tree/main/fugax) implements a sleek event loop 30 | that can be employed on various platforms. It provides time-based scheduling functionality and 31 | with a very enjoyable API and is used to orchestrate asynchronous task execution from a single, 32 | central control point. 33 | 34 | Fugax has an extensive test suite and excellent documentation. It has also been successfully 35 | used in commercial projects out in the wild. 36 | 37 | ### FUSS 38 | 39 | [FUSS](https://github.com/andsmedeiros/iara/tree/main/fuss) is an in-process pub/sub system. It 40 | allows for objects to subscribe to specific messages broadcast by other objects, even if all 41 | these objects are completely unrelated and know nothing more about each other, only by adhering 42 | to a type-based contract. 43 | 44 | FUSS is well tested and its code is well annotated; its usage guide is still lacking, however. 45 | Nonetheless, it has also been used successfully in commercial projects. 46 | 47 | ### Juro 48 | 49 | [Juro](https://github.com/andsmedeiros/iara/tree/main/juro) implements Javascript promises with 50 | a very strong typing assurance. It is light, efficient, cleverly implemented, and closely follows 51 | the original Javascript API. 52 | 53 | Juro's documentation is outstanding and its test suite is very comprehensive. It is the latest 54 | addition to my asynchrony-related libraries and the utter motivation for Iara to exist, but is 55 | already being employed on a commercial project that is about to be released. 56 | 57 | ### Plumbing 58 | 59 | [Plumbing](https://github.com/andsmedeiros/iara/tree/main/plumbing) is a legacy library with 60 | subpar functionality. It implements object pull streams, so that producers can yield their 61 | products to a pipeline of transformations, which will eventually lead to a consumer. 62 | 63 | Although the idea behind plumbing is excellent, its implementation is lacking and its API is not 64 | well thought. Also, there are no tests and no documentation, along a lot of dead and 65 | misfunctioning code. Even though I have used it in commercial projects, I do not feel its 66 | implementation complies to my standards nowadays, so I strongly discourage using it. 67 | 68 | **THEN WHY IS THIS S\*\*\* HERE???** 69 | 70 | It is here as a placeholder for the upcoming revamp this idea will get. The concept is good, 71 | back then I was not. 72 | 73 | ### Utils 74 | 75 | There are some [generic utils](https://github.com/andsmedeiros/iara/tree/main/utils) that 76 | are used through the framework, along with some legacy, unused, sometimes misfunctioning -- but 77 | still interesting, though -- code. This will eventually deserve a cleanup and some well care, 78 | however, it currently sits at the bottom of the list. 79 | 80 | ## Building 81 | 82 | The project builds by default with CMake and, besides Catch2 as the test library, there are no 83 | dependencies, so all the needed stuff is likely installed already. 84 | 85 | ``` 86 | ~$ git clone https://github.com/andsmedeiros/iara 87 | ~$ cd iara 88 | 89 | ~/iara$ mkdir build 90 | ~/iara$ cd build 91 | 92 | ~/iara/build$ cmake --preset=default .. 93 | ~/iara/build$ cmake --build . 94 | ``` 95 | 96 | This will create: 97 | ``` 98 | iara/ 99 | dist/ 100 | bin/ 101 | iara-test 102 | lib/ 103 | libfugax.[so/a] 104 | libjuro.[so/a] 105 | libiara.[so/a] 106 | ``` 107 | 108 | `iara-test` is the test suite; all `lib*` files are the library files that, in addition to 109 | include headers, are necessary to use each library. `libiara` is just the other libraries 110 | amalgamated, for now, and can be used along each library's include directory to provide all 111 | libraries at once. 112 | 113 | ### Build configuration 114 | 115 | Some build-time configuration is available. They can be customised by either providing a preset 116 | file 117 | 118 | ``` 119 | ~/iara/build$ cmake --preset=MY_PRESET_NAME .. 120 | ``` 121 | 122 | or passing environment variables during configuration phase 123 | 124 | ``` 125 | ~/iara/build$ OPTION_1=value1 OPTION_2=value2 cmake .. 126 | ``` 127 | 128 | At the moment, only Fugax employs this mechanism. These are the available build options: 129 | 130 | - `FUGAX_TIME_INCLUDE` if defined, will be directly appended to a `#include` directive and 131 | can be used to determine a header file that contains the definitions for Fugax's time 132 | type. 133 | - `FUGAX_MUTEX_INCLUDE` likewise, if defined, is expected to alias the path to a header file 134 | that contains Fugax's mutex type. 135 | - `FUGAX_TIME_TYPE` **[required]** must alias an integral, unsigned type to hold Fugax's 136 | internal counter. The choice of this type can interfere with the maximum delay an event 137 | can have. 138 | - `FUGAX_MUTEX_TYPE` the `BasicLockable` type that will be used to declare Fugax's event loop 139 | internal mutex. Even though name mutex, this can be any structure that can ensure a 140 | critical section does not get preempted, such as by disabling and re-enabling exceptions 141 | in embedded systems. 142 | 143 | ### Building without CMake 144 | 145 | Building without CMake is fairly easy: 146 | 147 | - Manually edit any files under `config/` tree with your desired configuration -- the files 148 | should be easy to understand and edit anyway --, then rename them from `*.hpp.in` to 149 | `*.hpp`. 150 | 151 | To target `libfugax`, compile everything under `fugax/src`, while including `fugax/include`, 152 | `config/include`, `juro/include` and `utils/include`. 153 | 154 | To target `libjuro`, compile everything under `juro/src`, while including `juro/include` and 155 | `utils/include`. 156 | 157 | FUSS is header-only and Plumbing is not a currently supported target. 158 | 159 | Each library has a test directory under `test/src` and `test/include`. Their test suites can be 160 | build using Catch2 and linking against their respective libraries, but this is unsupported out 161 | of CMake. -------------------------------------------------------------------------------- /test/src/fuss/test.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file test/src/fuss/test.cpp 3 | * @brief 4 | * @author André Medeiros 5 | * @date 08/08/23 6 | * @copyright 2023 (C) André Medeiros 7 | **/ 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | using namespace std::string_literals; 14 | using namespace utils::test_helpers; 15 | 16 | SCENARIO("a shouter can be created for messages of different types", "[fuss]") { 17 | GIVEN("various messages of various types (msg_1, msg_2, msg_3)") { 18 | struct msg_1 : public fuss::message<> { }; 19 | struct msg_2 : public fuss::message { }; 20 | struct msg_3 : public fuss::message { }; 21 | 22 | AND_GIVEN("a shouter type capable of shouting all these messages") { 23 | struct test_shouter : public fuss::shouter { }; 24 | 25 | WHEN("an instance of it is created") { 26 | auto create_result = attempt([&] { 27 | [[maybe_unused]] test_shouter shouter; 28 | }); 29 | 30 | THEN("an exception must not have been thrown") { 31 | REQUIRE_FALSE(create_result.has_error()); 32 | } 33 | } 34 | } 35 | } 36 | } 37 | 38 | SCENARIO("a shouter can be listened to and can shout", "[fuss]") { 39 | GIVEN("some message types and a shouter type") { 40 | struct msg_1 : public fuss::message<> { }; 41 | struct msg_2 : public fuss::message { }; 42 | struct test_shouter : public fuss::shouter { }; 43 | 44 | AND_GIVEN("a shouter instance") { 45 | test_shouter shouter; 46 | 47 | struct { 48 | int msg_2 = 0; 49 | int msg_1 = 0; 50 | } handler_count; 51 | 52 | AND_WHEN("its .listen() function is called with a functor") { 53 | auto listen_1_result = attempt([&] { 54 | return shouter.listen([&] { 55 | handler_count.msg_1++; 56 | }); 57 | }); 58 | 59 | THEN("no exception must have been thrown") { 60 | REQUIRE_FALSE(listen_1_result.has_error()); 61 | } 62 | 63 | THEN("it must have returned a message listener") { 64 | REQUIRE(listen_1_result.holds_value()); 65 | 66 | auto &listener = listen_1_result.get_value(); 67 | 68 | WHEN("the shouter's .shout() function is called") { 69 | auto shout_result = attempt([&] { 70 | shouter.shout(); 71 | }); 72 | 73 | THEN("no exception must have been thrown") { 74 | REQUIRE_FALSE(shout_result.has_error()); 75 | } 76 | 77 | THEN("the functor provided as msg_1's handler must have been invoked") { 78 | REQUIRE(handler_count.msg_1 == 1); 79 | } 80 | } 81 | 82 | WHEN("the listener's .cancel() function is called") { 83 | auto cancel_result = attempt([&] { 84 | listener.cancel(); 85 | }); 86 | 87 | THEN("no exception must have been thrown") { 88 | REQUIRE_FALSE(cancel_result.has_error()); 89 | } 90 | 91 | AND_WHEN("the shouter shouts msg_1") { 92 | shouter.shout(); 93 | 94 | THEN("the functor provided as msg_1's handler must not have been invoked") { 95 | REQUIRE(handler_count.msg_1 == 0); 96 | } 97 | } 98 | } 99 | } 100 | 101 | AND_WHEN("the shouter's msg_2 is being listened") { 102 | std::string shouted_string; 103 | auto shout_2_listener_1 = 104 | shouter.listen([&] (const auto &value) { 105 | handler_count.msg_2++; 106 | shouted_string = value; 107 | }); 108 | 109 | AND_WHEN("the shouter shouts msg_2") { 110 | shouter.shout("message 2 shouted"s); 111 | 112 | THEN("the attached handler must have been executed") { 113 | REQUIRE(handler_count.msg_2 == 1); 114 | REQUIRE(shouted_string == "message 2 shouted"s); 115 | } 116 | 117 | THEN("any handlers attached to msg_1 must not have been executed") { 118 | REQUIRE(handler_count.msg_1 == 0); 119 | } 120 | } 121 | 122 | AND_WHEN("another handler is attached to msg_2") { 123 | auto shouter_2_listener_2 = 124 | shouter.listen([&] (auto) { 125 | handler_count.msg_2++; 126 | }); 127 | 128 | AND_WHEN("the shouter shouts msg_2") { 129 | shouter.shout("message 2 shouted"s); 130 | 131 | THEN("both handlers must have been executed") { 132 | REQUIRE(handler_count.msg_2 == 2); 133 | REQUIRE(shouted_string == "message 2 shouted"s); 134 | } 135 | 136 | THEN("any handlers attached to msg_1 must not have been executed") { 137 | REQUIRE(handler_count.msg_1 == 0); 138 | } 139 | } 140 | } 141 | } 142 | } 143 | } 144 | } 145 | } 146 | 147 | SCENARIO("a message guard can manage the lifetime of a listener", "[fuss]") { 148 | GIVEN("a one-message shouter") { 149 | struct msg : public fuss::message<> { }; 150 | struct test_shouter : public fuss::shouter { }; 151 | 152 | test_shouter shouter; 153 | 154 | WHEN("it is listened to") { 155 | bool handler_executed = false; 156 | auto listener = shouter.listen([&] { 157 | handler_executed = true; 158 | }); 159 | 160 | AND_WHEN("it shouts") { 161 | shouter.shout(); 162 | 163 | THEN("the attached handler must have been executed") { 164 | REQUIRE(handler_executed); 165 | } 166 | } 167 | 168 | AND_WHEN("the returned listener is yielded to a message guard") { 169 | auto guard_result = attempt([&] { 170 | return new fuss::message_guard { std::move(listener) }; 171 | }); 172 | 173 | THEN("no exception must have been thrown") { 174 | REQUIRE_FALSE(guard_result.has_error()); 175 | 176 | auto *guard = guard_result.get_value(); 177 | 178 | AND_WHEN("the message is shouted") { 179 | shouter.shout(); 180 | 181 | THEN("the attached handler must have been executed") { 182 | REQUIRE(handler_executed); 183 | } 184 | } 185 | 186 | AND_WHEN("the message guard is destroyed") { 187 | auto delete_result = attempt([&] { 188 | delete guard; 189 | }); 190 | 191 | THEN("no exception must have been thrown") { 192 | REQUIRE_FALSE(delete_result.has_error()); 193 | } 194 | 195 | AND_WHEN("the message is shouted") { 196 | shouter.shout(); 197 | 198 | THEN("the attached handler must not have been executed") { 199 | REQUIRE_FALSE(handler_executed); 200 | } 201 | } 202 | } 203 | } 204 | } 205 | } 206 | } 207 | } 208 | 209 | SCENARIO("shouter groups can aggregate multiple shouter inheritance chains", "[fuss]") { 210 | GIVEN("a shouter type") { 211 | struct msg_1 : public fuss::message<> { }; 212 | struct base_shouter : public fuss::shouter { }; 213 | 214 | AND_GIVEN("another message type") { 215 | struct msg_2 : public fuss::message<> { }; 216 | 217 | THEN("it is possible to create a group of shouters type") { 218 | struct group_shouter : 219 | public fuss::group> { }; 220 | 221 | AND_WHEN("an instance is constructed") { 222 | auto construct_result = attempt([] { 223 | [[maybe_unused]] group_shouter shouter; 224 | }); 225 | 226 | THEN("no exception must have been thrown") { 227 | REQUIRE_FALSE(construct_result.has_error()); 228 | } 229 | } 230 | 231 | AND_GIVEN("an instance of it") { 232 | group_shouter shouter; 233 | 234 | THEN("it is possible to listen for both messages") { 235 | struct { 236 | bool msg_1 = false; 237 | bool msg_2 = false; 238 | } handler_executed; 239 | shouter.listen([&] { 240 | handler_executed.msg_1 = true; 241 | }); 242 | shouter.listen([&] { 243 | handler_executed.msg_2 = true; 244 | }); 245 | 246 | AND_WHEN("the shouter shouts msg_1") { 247 | shouter.shout(); 248 | 249 | THEN("the attached handler must have been executed") { 250 | REQUIRE(handler_executed.msg_1); 251 | } 252 | } 253 | 254 | AND_WHEN("the shouter shouts msg_2") { 255 | shouter.shout(); 256 | 257 | THEN("the attached handler must have been executed") { 258 | REQUIRE(handler_executed.msg_2); 259 | } 260 | } 261 | } 262 | } 263 | } 264 | } 265 | } 266 | } 267 | 268 | SCENARIO("exceptions thrown inside message handlers must propagate") { 269 | GIVEN("a shouter") { 270 | struct msg : public fuss::message<> { }; 271 | struct test_shouter : public fuss::shouter { }; 272 | 273 | test_shouter shouter; 274 | 275 | WHEN("it is attached a handler that throws an exception") { 276 | auto listen_result = attempt([&] { 277 | shouter.listen([] { 278 | throw std::runtime_error { "handler exception"s }; 279 | }); 280 | }); 281 | 282 | THEN("no exception must have been thrown") { 283 | REQUIRE_FALSE(listen_result.has_error()); 284 | } 285 | 286 | AND_WHEN("the message is shouted") { 287 | auto shout_result = attempt([&] { 288 | shouter.shout(); 289 | }); 290 | 291 | THEN("the exception from the handler must have been propagated") { 292 | REQUIRE(shout_result.holds_error()); 293 | REQUIRE( 294 | shout_result.get_error().what() == 295 | "handler exception"s 296 | ); 297 | } 298 | } 299 | } 300 | } 301 | } -------------------------------------------------------------------------------- /fugax/include/fugax/event-loop.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file fugax/include/fugax/event-loop.hpp 3 | * @brief Contains definitions for the event loop and auxiliary constructs 4 | * @author André Medeiros 5 | * @date 22/06/23 6 | * @copyright 2023 (C) André Medeiros 7 | **/ 8 | 9 | #ifndef FUGAX_EVENT_LOOP_HPP 10 | #define FUGAX_EVENT_LOOP_HPP 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include "event.hpp" 25 | #include "event-listener.hpp" 26 | #include "event-guard.hpp" 27 | 28 | namespace fugax { 29 | using namespace config::fugax; 30 | using namespace utils::types; 31 | 32 | /** 33 | * @brief Tag struct to indicate an asynchronous timeout has occurred 34 | */ 35 | struct timeout { }; 36 | 37 | /** 38 | * @brief Represents all possible forms of scheduling an event in the 39 | * event loop 40 | */ 41 | enum class schedule_policy { 42 | /** 43 | * @brief Will execute the task as soon as possible 44 | */ 45 | immediate, 46 | /** 47 | * @brief Will execute the task after some time 48 | */ 49 | delayed, 50 | /** 51 | * @brief Will execute the task periodically, being the first execution 52 | * as soon as possible 53 | */ 54 | recurring_immediate, 55 | /** 56 | * @brief Will execute the task periodically, being the first execution 57 | * after some time 58 | */ 59 | recurring_delayed, 60 | /** 61 | * @brief Will execute the task **every** runloop, until it is cancelled 62 | */ 63 | always 64 | }; 65 | 66 | /** 67 | * @brief An event loop is an object that coordinates execution of tasks. 68 | * @details The event loop has the ability to receive functors and store 69 | * them in varying ways to be able to execute them later. It can schedule 70 | * both recurring and one-shot tasks for either semi-immediate or delayed 71 | * execution. 72 | * Also it contains conveniency functions that deal with promises in a 73 | * temporal perspective. 74 | */ 75 | class event_loop { 76 | /** 77 | * @brief Queues of events are stored internally as linked lists of 78 | * shared pointers to events. This allows us to efficiently enqueue 79 | * events and merge queues. 80 | * The choice for the shared pointer occurs because it allows for the 81 | * event lifetime to be safely detached from the lifetime of event 82 | * listeners spread across the application. This makes both disposing 83 | * of events from inside the event loop and attempting to cancel them 84 | * from outside the event loop safe. 85 | */ 86 | using event_queue = std::list>; 87 | 88 | /** 89 | * @brief All events are stored in the timer map. It contains a 90 | * collection of entries associating a task to its due time. When 91 | * the due time arrives, the task gets executed and, optionally, 92 | * gets re-scheduled then. 93 | * The unsigned integer type used represent timepoints can be configured 94 | * via the macro `FUGAX_TIME_TYPE`. 95 | */ 96 | using timer_map = std::map; 97 | 98 | /** 99 | * @brief A mutex to lock scheduling operations. 100 | * @attention If `std::mutex` is unavailable in your platform, 101 | * supply via configuration an object type that responds to `.lock()` 102 | * and `.unlock()` and can ensure proper mutual exclusion. 103 | * E.g.: in bare-metal platforms, provide the macro `FUGAX_MUTEX_TYPE` 104 | * a custom mutex object type that, when locked, saves interruption 105 | * state and disabled them and, when unlock, restores interruption 106 | * state. 107 | */ 108 | mutex_type mutex; 109 | 110 | /** 111 | * @brief Stores scheduled events, indexed by their due times. 112 | */ 113 | timer_map timers; 114 | 115 | /** 116 | * @brief Keeps current execution time. As this value is updated, events 117 | * get executed if it matches their due time. 118 | */ 119 | time_type counter = 0; 120 | 121 | public: 122 | /** 123 | * @brief Main management function. Inform the loop of time passing. 124 | * By giving an update on what time is it now, instructs the loop to 125 | * execute all due tasks. 126 | * @param now New value for the internal time counter 127 | */ 128 | void process(time_type now); 129 | 130 | /** 131 | * @brief Schedules a task for immediate execution 132 | * @param functor The task functor 133 | * @return An event listener that can be used to cancel the event 134 | * @see `fugax::event_loop::schedule(delay, policy, functor)` 135 | */ 136 | event_listener schedule(event_handler functor); 137 | 138 | /** 139 | * @brief Schedules a task for delayed execution 140 | * @param delay How many units of time to delay execution 141 | * @param functor The task functor 142 | * @return An event listener that can be used to cancel the event 143 | * @see `fugax::event_loop::schedule(delay, policy, functor)` 144 | */ 145 | event_listener schedule(time_type delay, event_handler functor); 146 | 147 | /** 148 | * @brief Schedules a task for delayed (and optionally recurring) execution 149 | * @param delay How many units of time to delay execution; if `recurring` 150 | * is true, this determines the period between two successive calls. 151 | * @param recurring If true, this function will be executed periodically 152 | * @param functor The task functor 153 | * @return An event listener that can be used to cancel the event 154 | * @see `fugax::event_loop::schedule(delay, policy, functor)` 155 | */ 156 | event_listener schedule(time_type delay, bool recurring, event_handler functor); 157 | 158 | /** 159 | * @brief Schedules a task for eventual execution according to a supplied policy 160 | * @details This is the most flexible scheduling function available. It takes 161 | * a `policy` parameter that yield different behaviours by the event loop: 162 | * - If policy is `immediate`, the functor is enqueued for single execution in 163 | * the current time slot. The `delay` parameter is ignored. 164 | * - If policy is `delayed`, the functor is enqueued for single execution in a 165 | * future time slot. The `delay` parameter determines how many units of time 166 | * in the future to schedule this task. 167 | * - If policy is `recurring_immediate`, the functor is enqueued for execution 168 | * in the current time slot and will be continuously rescheduled for execution 169 | * in intervals defined by the `delay` parameter. 170 | * - If policy is `recurring_delayed`, the functor is enqueued for execution in 171 | * a future time slot and will be continuously rescheduled for execution 172 | * in intervals defined by the `delay` parameter. 173 | * - If policy is `always`, the functor will be executed on every call to 174 | * `.process()`, a.k.a. runloop . This is useful to bridge asynchronous and 175 | * synchronous behaviour, but will incur considerable overhead to the loop 176 | * latency if used incautiously. 177 | * @param delay How many units of time to delay execution; depending on the 178 | * provided policy, this also determines the period between two successive calls 179 | * @param policy How this task is to be scheduled 180 | * @param functor The task functor 181 | * @return An event listener that can be used to cancel the event 182 | */ 183 | event_listener schedule(time_type delay, schedule_policy policy, event_handler functor); 184 | 185 | /** 186 | * @brief Schedules a task for continuous execution: it will be invoked every 187 | * @param functor The task functor 188 | * @return An event listener that can be used to cancel the event 189 | * @see `fugax::event_loop::schedule(delay, policy, functor)` 190 | */ 191 | event_listener always(event_handler functor); 192 | 193 | /** 194 | * @brief Creates a new promise that resolves after some time 195 | * @param delay The delay until the promise resolution 196 | * @return The new promise pointer 197 | */ 198 | juro::promise_ptr wait(time_type delay); 199 | 200 | /** 201 | * @brief Returns a mutable lambda that can be called multiple times, 202 | * but will only execute the provided functor after some time of the 203 | * last call 204 | * @details `debounce` will create a new lambda that, when called, 205 | * will schedule a new event containing the provided functor to 206 | * be executed after `delay`; however, if execution is already 207 | * pending from a previously call, it is instead rescheduled, so 208 | * the provided functor is actually only executed once `delay` has 209 | * passed without any calls to the functor. 210 | * @tparam T_args A parameter pack that defines what arguments the 211 | * lambda accepts and forwards to the functor 212 | * @tparam T_functor The type of the functor to be invoked when the 213 | * call is finally debounced 214 | * @param delay The time during which the lambda must not be called 215 | * for the provided functor to be invoked 216 | * @param functor The functor to be executed 217 | * @return A new lambda that can be called repeatedly with `T_args...&` 218 | */ 219 | template 220 | auto debounce(time_type delay, T_functor &&functor) { 221 | return [ 222 | this, 223 | delay, 224 | functor = std::forward(functor), 225 | guard = std::make_shared(), 226 | stored_args = std::optional> { } 227 | ] (T_args ...args) mutable { 228 | stored_args = std::make_tuple(std::move(args)...); 229 | 230 | if(const auto ev = guard->get().lock()) { 231 | ev->reschedule(counter + delay); 232 | } else { 233 | *guard = schedule(delay, [&] { 234 | std::apply(functor, *stored_args); 235 | }); 236 | } 237 | }; 238 | } 239 | 240 | /** 241 | * @brief Returns a mutable lambda that can be called multiple times, 242 | * but will only execute the provided functor from time to time, 243 | * silently swallowing intermediary calls 244 | * @details `throttle` will create a new lambda that, when called, 245 | * will invoke the provided functor and schedule a new event to 246 | * notify it that `delay` has passed; until it is notified, any calls 247 | * to the lambda will be ignored 248 | * @tparam T_args A parameter pack that defines what arguments the 249 | * lambda accepts and forwards to the functor 250 | * @tparam T_functor The type of the functor to be invoked 251 | * @param delay The minimum interval between two successive calls; also 252 | * the time while the lambda remains "disarmed" 253 | * @param functor The functor to be executed 254 | * @return A new lambda that can be called repeatedly with `T_args...&` 255 | */ 256 | template 257 | auto throttle(time_type delay, T_functor &&functor) { 258 | return [ 259 | this, 260 | delay, 261 | functor = std::forward(functor), 262 | guard = std::make_shared(), 263 | armed = true 264 | ] (T_args ...args) mutable { 265 | if(armed) { 266 | armed = false; 267 | *guard = schedule(delay, [&] { armed = true; }); 268 | functor(std::move(args)...); 269 | } 270 | }; 271 | } 272 | 273 | /** 274 | * @brief Creates a new promise that resolves either when the provided 275 | * promise is resolved or when the requested delay has passed 276 | * @tparam T_value The type of the promise to race against 277 | * @param delay The maximum time to wait fot the task to complete 278 | * @param promise The promise representing the asynchronous task 279 | * @return A new race promise that represents the race 280 | */ 281 | template 282 | inline auto timeout(time_type delay, const juro::promise_ptr &promise) { 283 | return juro::race(promise, wait(delay)); 284 | } 285 | 286 | /** 287 | * @brief Shortcut for creating a new promise and providing it to 288 | * fugax::event_loop::timeout(time_type, const juro_promise_ptr &) 289 | * @tparam T_value The type of the promise being created 290 | * @tparam T_launcher The type launcher functor of the promise being created 291 | * @param delay Time limit to wait for the promise to resolve 292 | * @param launcher The launcher functor to be supplied to 293 | * juro::make_promise 294 | * @return A new race promise 295 | * @see fugax::event_loop::timeout(time_type, const juro_promise_ptr &) 296 | */ 297 | template 298 | inline auto timeout(time_type delay, const T_launcher &launcher) { 299 | return timeout(delay, juro::make_promise(launcher)); 300 | } 301 | 302 | private: 303 | /** 304 | * @brief Collects from the timer map all events that are due; time entries 305 | * with a value different than the current counter will be deleted from the map 306 | * @return All events whose scheduled time is less than or equal to the current 307 | * counter as an `event_queue` 308 | */ 309 | event_queue get_due_timers(time_type) noexcept; 310 | }; 311 | 312 | } /* namespace fugax */ 313 | 314 | #endif /* FUGAX_EVENT_LOOP_HPP */ -------------------------------------------------------------------------------- /fuss/include/fuss.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file fuss/include/fuss.hpp 3 | * @brief Contains all necessary definitions for FUSS 4 | * @author André Medeiros 5 | * @date 26/06/23 6 | * @copyright 2023 (C) André Medeiros 7 | **/ 8 | 9 | #ifndef FUSS_HPP 10 | #define FUSS_HPP 11 | 12 | #include 13 | #include 14 | 15 | namespace fuss { 16 | 17 | template 18 | class shouter; 19 | 20 | /** 21 | * @brief Defines a generic cancellable interface 22 | */ 23 | class cancellable { 24 | public: 25 | 26 | /** 27 | * @brief Should cancel a derived construct 28 | */ 29 | virtual void cancel() = 0; 30 | virtual ~cancellable() noexcept = default; 31 | }; 32 | 33 | /** 34 | * @brief A message is a type-based contract 35 | * @tparam T_args The type signature of this message: any shouter must 36 | * be able to produce `T_args...` when calling `fuss::shouter::shout()` and 37 | * and any listener must be able to consume `T_args...` through the functor 38 | * that it provides 39 | */ 40 | template 41 | class message { 42 | public: 43 | class handler; 44 | 45 | /** 46 | * @brief Each message type has a unique handler type; this aliases a list 47 | * of that unique handler type 48 | */ 49 | using handler_list = std::list>; 50 | 51 | /** 52 | * @brief The unique handler type for this specific message type 53 | */ 54 | class handler : public cancellable { 55 | template friend class shouter; 56 | 57 | protected: 58 | /** 59 | * @brief Keeps a reference of the list where this handler is stored for 60 | * when the handler is unregistered 61 | */ 62 | handler_list &source; 63 | 64 | /** 65 | * @brief An iterator that points to this element in a list 66 | */ 67 | typename handler_list::iterator element; 68 | 69 | /** 70 | * @brief Creates a new message handler to be inserted in the provided list 71 | * @param source The list that will contain this new handler 72 | */ 73 | handler(handler_list &source) : 74 | source { source }, 75 | element { source.end() } 76 | { } 77 | 78 | /** 79 | * @brief Message handler calling interface 80 | * @param args The arguments with which to invoke this handler 81 | */ 82 | virtual void operator()(T_args...args) = 0; 83 | 84 | public: 85 | virtual ~handler() noexcept = default; 86 | 87 | /** 88 | * @brief Cancels this handler by removing it from the list where it is 89 | * stored 90 | */ 91 | virtual void cancel() noexcept override { 92 | if(element != source.end()) { 93 | source.erase(element); 94 | element = source.end(); 95 | } 96 | } 97 | 98 | private: 99 | /** 100 | * @brief Informs the handler of where it was stored 101 | * @param it The iterator that points to this handler 102 | */ 103 | void attach(typename handler_list::iterator it) { 104 | element = it; 105 | } 106 | }; 107 | 108 | /** 109 | * @brief Stores the concrete implementation of a handler functor 110 | * @tparam T The type of the functor wrapped by this object 111 | */ 112 | template 113 | class functor : public handler { 114 | /** 115 | * @brief This is the actual callable object that gets executed 116 | */ 117 | T t; 118 | 119 | public: 120 | /** 121 | * @brief Constructs a new wrapper around the supplied functor object 122 | * @tparam T_func The type of the functor being wrapped 123 | * @param source The list into which this functor will be placed 124 | * @param func The functor to be wrapped 125 | */ 126 | template 127 | functor(handler_list &source, T_func &&func) : 128 | handler { source }, 129 | t { std::forward(func) } 130 | { } 131 | 132 | functor(const functor &) 133 | noexcept(std::is_nothrow_copy_constructible_v) = default; 134 | 135 | functor(functor &&) 136 | noexcept(std::is_nothrow_move_constructible_v) = default; 137 | 138 | functor &operator=(const functor &) 139 | noexcept(std::is_nothrow_copy_assignable_v) = default; 140 | 141 | functor &operator=(functor &&) 142 | noexcept(std::is_nothrow_move_assignable_v) = default; 143 | 144 | /** 145 | * @brief Invokes the wrapped functor with the provided arguments 146 | * @param args The arguments that will be forwarded to the functor 147 | */ 148 | virtual void operator()(T_args ...args) override { 149 | t(std::move(args)...); 150 | } 151 | }; 152 | }; 153 | 154 | /** 155 | * @brief A message listener keeps a weak pointer to a message handler attached 156 | * to a particular message shouter, so the subscription can be cancelled 157 | * safely anytime 158 | */ 159 | class listener { 160 | /** 161 | * @brief Keeps a weak reference to the message handler 162 | */ 163 | std::weak_ptr handler; 164 | 165 | public: 166 | /** 167 | * @brief Constructs a new empty listener, that points to no handler 168 | */ 169 | inline listener() noexcept = default; 170 | 171 | /** 172 | * @brief Creates a new listener out a shared pointer to a handler 173 | * @param handler A shared pointer to a handler object, cast to a 174 | * cancellable interface 175 | */ 176 | inline listener(const std::shared_ptr &handler) noexcept : 177 | handler { handler } 178 | { } 179 | 180 | 181 | listener(const listener &) noexcept = default; 182 | listener(listener &&) noexcept = default; 183 | virtual ~listener() noexcept = default; 184 | 185 | listener &operator=(const listener &) noexcept = default; 186 | listener &operator=(listener &&) noexcept = default; 187 | 188 | /** 189 | * @brief Attempts to lock the weak pointer and cancel the message 190 | * handler by removing it from the shouter's handler list 191 | */ 192 | inline void cancel() const noexcept { 193 | if(auto h = handler.lock()) { 194 | h->cancel(); 195 | } 196 | } 197 | }; 198 | 199 | /** 200 | * @brief RAII-enabled container for a message listener; when it 201 | * goes out of scope, it cancels the handler pointer by the listener 202 | */ 203 | class message_guard : public listener { 204 | public: 205 | using listener::operator=; 206 | 207 | message_guard() noexcept = default; 208 | 209 | /** 210 | * @brief Message guards are move-only, so its copy constructor is 211 | * deleted 212 | */ 213 | message_guard(const message_guard &) = delete; 214 | message_guard(message_guard &&) noexcept = default; 215 | 216 | /** 217 | * @brief Converting constructor; allows to seemingly cast a message 218 | * listener to a message guard via copy-construction 219 | * @param listener The message listener 220 | */ 221 | inline message_guard(const fuss::listener &listener) : 222 | fuss::listener { listener } 223 | { } 224 | 225 | /** 226 | * @brief Converting constructor; allows to seemingly cast a message 227 | * listener to a message guard via move-construction 228 | * @param listener The message listener 229 | */ 230 | inline message_guard(fuss::listener &&listener) noexcept : 231 | fuss::listener { std::move(listener) } 232 | { } 233 | 234 | /** 235 | * @brief Copy-assignment deleted because message guards are 236 | * move-only objects 237 | */ 238 | message_guard &operator=(const message_guard &) = delete; 239 | message_guard &operator=(message_guard &&) noexcept = default; 240 | 241 | /** 242 | * @brief Releases the guard: attempt to cancel the message handler 243 | * apointer by the listener 244 | */ 245 | inline void release() const noexcept { cancel(); } 246 | 247 | /** 248 | * @brief Upon destruction, release this guard 249 | */ 250 | ~message_guard() noexcept { 251 | release(); 252 | } 253 | }; 254 | 255 | /** 256 | * @brief A shouter is an actor that can broadcast messages containing 257 | * arbitrary data to interested parties, who react to the messages through 258 | * attached message handlers 259 | * @attention this is a proxy type that can produce a single 260 | * shouter class for the message classes in 261 | * @tparam T_message The first message in the pack 262 | * @tparam T_rest The rest of the messages in the pack 263 | */ 264 | template 265 | struct shouter : public shouter, public shouter { 266 | using shouter::listen; 267 | using shouter::shout; 268 | using shouter::listen; 269 | using shouter::shout; 270 | }; 271 | 272 | /** 273 | * @brief A shouter is an actor that can broadcast messages containing 274 | * arbitrary data to interested parties, who react to the messages through 275 | * attached message handlers 276 | * @tparam T_message The type of the message this object can shout 277 | */ 278 | template 279 | class shouter { 280 | public: 281 | /** 282 | * @brief The type of the handler is provided by the message type 283 | */ 284 | using handler = typename T_message::handler; 285 | 286 | /** 287 | * @brief The type of the functor object is provided by the message type 288 | */ 289 | template using functor = typename T_message::template functor; 290 | 291 | /** 292 | * @brief Represents a list of message handlers 293 | */ 294 | using handler_list = typename T_message::handler_list; 295 | 296 | /** 297 | * @brief The list of handlers registered at this shouter; whenever `.shout()` 298 | * is called, each handler in this list will be invoked 299 | */ 300 | handler_list handlers; 301 | 302 | public: 303 | 304 | /** 305 | * @brief Attaches a new message handler to this shouter and returns the 306 | * message listener that represents this subscription 307 | * @tparam T_msg The type of the message that is being shouted; this parameter 308 | * is used to disambiguate between the multiple `.shout()` functions a single 309 | * shouter can have 310 | * @tparam T The type of the functor to be executed when the message handler 311 | * is called 312 | * @param t The handler functor 313 | * @return A message listener that can be used to cancel this subscription 314 | */ 315 | template 316 | std::enable_if_t, listener> 317 | listen(T &&t) { 318 | auto func = 319 | std::make_shared>>(handlers, std::forward(t)); 320 | auto iterator = handlers.emplace(handlers.end(), std::move(func)); 321 | (*iterator)->attach(iterator); 322 | 323 | return std::static_pointer_cast(*iterator); 324 | } 325 | 326 | /** 327 | * @brief Broadcasts a message, calling each message handler in this shouter's 328 | * list with the provided arguments 329 | * @tparam T_msg The type of the message to shout; this parameter is used to 330 | * disambiguate between the multiple `.listen()` functions a single shouter 331 | * can have 332 | * @tparam T_args The type of the parameters that will be handled to each handler 333 | * @param args The arguments used to call each handler 334 | */ 335 | template 336 | std::enable_if_t> 337 | shout(T_args &&...args) { 338 | for(auto &handler : handlers) { 339 | (*handler)(args...); 340 | } 341 | } 342 | }; 343 | 344 | /** 345 | * @brief Utility metatemplate that installs a trampoline function on a derived class 346 | * @details This class is used when implementing a class that inherits from both 347 | * `shouter` and a class derived from `shouter`. In this case, the compiler is unable 348 | * to disambiguate between their `listen()` and `shout()` functions unless explicitly 349 | * informed of whose shouter ancestor's function to call. This class installs a 350 | * trampoline version of `.listen()` and `.shout()` that does this disambiguation 351 | * @tparam T_shouters The type of the shouters that will compose this group 352 | */ 353 | template 354 | struct group : public T_shouters... { 355 | 356 | /** 357 | * @brief Explicitly calls `shouter::template shout(T_args...)` 358 | * @tparam T_message The type of the message being shouted 359 | * @tparam T_args The type of the arguments being forwarded to the shouter 360 | * @param args The provided arguments 361 | */ 362 | template 363 | void shout(T_args &&...args) { 364 | shouter::template shout(std::forward(args)...); 365 | } 366 | 367 | /** 368 | * @brief Explicitly calls `shouter::template listen(T_functor)` 369 | * @tparam T_message The type of the message for which to listen 370 | * @tparam T_functor The type of the functor to forward to the shouter 371 | * @param fun The functor object to be used in the handler constructor 372 | * @return The message listener returned by the shouter 373 | */ 374 | template 375 | auto listen(T_functor &&fun) { 376 | return shouter:: 377 | template listen(std::forward(fun)); 378 | } 379 | }; 380 | 381 | } /* namespace fuss */ 382 | 383 | #endif /* FUSS_HPP */ 384 | -------------------------------------------------------------------------------- /juro/include/juro/helpers.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file juro/helpers.hpp 3 | * @brief Contains type helpers used throughout the library 4 | * @author André Medeiros 5 | */ 6 | 7 | #ifndef JURO_HELPERS_HPP 8 | #define JURO_HELPERS_HPP 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace juro { 17 | template class promise; 18 | } /* namespace juro */ 19 | 20 | namespace juro::helpers { 21 | 22 | /** 23 | * @brief A shared pointer to a `promise` 24 | * @tparam T The type of the promised value 25 | */ 26 | template 27 | using promise_ptr = std::shared_ptr>; 28 | 29 | /** 30 | * @brief The exception error thrown by invalid promise operations 31 | */ 32 | struct promise_error : std::runtime_error { 33 | using std::runtime_error::runtime_error; 34 | }; 35 | 36 | /** 37 | * @brief The possible states of a promise at one given time 38 | */ 39 | enum class promise_state { PENDING, RESOLVED, REJECTED }; 40 | 41 | /** 42 | * @brief Tag type to disambiguate the settled promise constructors call. This 43 | * represents the resolved state. 44 | */ 45 | struct resolved_promise_tag { }; 46 | 47 | /** 48 | * @brief Tag type to disambiguate the settled promise constructors call. This 49 | * represents the rejected state. 50 | */ 51 | struct rejected_promise_tag { }; 52 | 53 | /** 54 | * @brief Tag struct to represent an absent value. This is used by pending 55 | * promises as a value placeholder. 56 | */ 57 | struct empty_type { }; 58 | 59 | /** 60 | * @brief Tag struct to represent a void value. This is used by void promises 61 | * as a value placeholder. 62 | */ 63 | struct void_type { 64 | friend inline bool operator==(const void_type &, const void_type &) noexcept { 65 | return true; 66 | } 67 | }; 68 | 69 | /** 70 | * @brief Defines a concrete type that can hold the value of a promise. 71 | * @details As `void` is an incomplete type, if `promise` had a `void` 72 | * member, the program would be ill-formed. This alias maps `void` to 73 | * `void_type` and every other type to itself, so both void and non-void 74 | * promises can have a value member. 75 | * @tparam T Type type of the promise 76 | */ 77 | template 78 | using storage_type = std::conditional_t, void_type, T>; 79 | 80 | /** 81 | * @brief Defines a container suitable to hold two different types. 82 | * @details This clause activates when both types are diferent. The internal 83 | * `type` is a `std::variant`. 84 | * @tparam T1 85 | * @tparam T2 86 | */ 87 | template 88 | struct common_container { 89 | using type = std::variant; 90 | }; 91 | 92 | /** 93 | * @brief Defines a container suitable to hold two different types. 94 | * @details This clause activates when the first type is `void`. The internal 95 | * `type` is a `std::optional`. 96 | * @tparam T The non-void type 97 | */ 98 | template 99 | struct common_container { 100 | using type = std::optional; 101 | }; 102 | 103 | /** 104 | * @brief Defines a container suitable to hold two different types. 105 | * @details This clause activates when the second type is `void`. The internal 106 | * `type` is a `std::optional`. 107 | * @tparam T The non-void type 108 | */ 109 | template 110 | struct common_container { 111 | using type = std::optional; 112 | }; 113 | 114 | /** 115 | * @brief Defines a container suitable to hold two different types. 116 | * @details This clause activates both types are `void`. The internal `type` 117 | * is a `void`. 118 | * @tparam T The non-void type 119 | */ 120 | template<> 121 | struct common_container { 122 | using type = void; 123 | }; 124 | 125 | /** 126 | * @brief Defines a container suitable to hold two different types. 127 | * @details This clause activates when both types are the same. The internal 128 | * `type` is a `T`. 129 | * @tparam T The type supplied to both parameters 130 | */ 131 | template 132 | struct common_container { 133 | using type = T; 134 | }; 135 | 136 | /** 137 | * @brief Helper type that aliases `common_container::type`. 138 | * @tparam T1 139 | * @tparam T2 140 | */ 141 | template 142 | using common_container_t = typename common_container::type; 143 | 144 | /** 145 | * @brief Yields a non-promise type suitable for chained promise resolution. 146 | * This clause activates when the provided type is already not a promise. 147 | * @tparam T The type to attempt unwrapping 148 | */ 149 | template 150 | struct unwrap_if_promise { 151 | using type = T; 152 | }; 153 | 154 | /** 155 | * @brief Yields a non-promise type suitable for chained promise resolution. 156 | * This clause activates when the provided type is a promise and aliases the 157 | * promises's type. 158 | * @tparam T The type to attempt unwrapping 159 | */ 160 | template 161 | struct unwrap_if_promise> { 162 | using type = typename unwrap_if_promise::type; 163 | }; 164 | 165 | /** 166 | * @brief Helper alias to `unwrap_if_promise::type` 167 | * @tparam T The type to attempt unwrapping 168 | */ 169 | template 170 | using unwrap_if_promise_t = typename unwrap_if_promise::type; 171 | 172 | /** 173 | * @brief Type trait to detect if a type is a promise. This clause activates 174 | * when supplied with a non-promise type. 175 | * @tparam T The type to inspect 176 | */ 177 | template 178 | struct is_promise : public std::false_type { }; 179 | 180 | /** 181 | * @brief Type trait to detect if a type is a promise. This clause activates 182 | * when supplied with a promise type. 183 | * @tparam T The type to inspect 184 | */ 185 | template 186 | struct is_promise> : public std::true_type { }; 187 | 188 | /** 189 | * @brief Helper constexpr bool to detect if a given type is a promise 190 | * @tparam T The type to inspect 191 | */ 192 | template 193 | static constexpr inline bool is_promise_v = is_promise::value; 194 | 195 | /** 196 | * @brief Type trait to determine what type does a resolve handler returns. This 197 | * clause activates for non-void promises. 198 | * @tparam T The promise type 199 | * @tparam T_on_resolve The resolve handler type 200 | */ 201 | template 202 | struct resolve_result { 203 | using type = std::invoke_result_t; 204 | }; 205 | 206 | /** 207 | * @brief Type trait to determine what type does a resolve handler returns. This 208 | * clause activates for void promises. 209 | * @tparam T_on_resolve The resolve handler type 210 | */ 211 | template 212 | struct resolve_result { 213 | using type = std::invoke_result_t; 214 | }; 215 | 216 | /** 217 | * @brief Helper alias to `resolve_result::type` 218 | * @tparam T The promise type 219 | * @tparam T_on_resolve The resolve handler type 220 | */ 221 | template 222 | using resolve_result_t = typename resolve_result::type; 223 | 224 | /** 225 | * @brief Helper alias to 226 | * `std::invoke_result_t`. Yields the type a 227 | * reject handler returns. 228 | * @tparam T_on_reject The reject handler type 229 | */ 230 | template 231 | using reject_result_t = std::invoke_result_t; 232 | 233 | /** 234 | * @brief Determines the type of the promise that should be returned by a call 235 | * to `.then()`, `rescue()` or `finally()`. 236 | * @details When calling `.then()`, `.rescue()` or `.finally()`, a new promise 237 | * is returned. The type of this promise is determined based on what each 238 | * attached handler returns, as follows: 239 | * 240 | * - The resolve and reject handlers' returning types are extracted as T1 and 241 | * T2. 242 | * - T1 and T2 are unwrapped into U1 and U2: if any of them is a promise, it is 243 | * mapped to the promise's type, otherwise, it is mapped to itself. 244 | * - If U1 and U2 are the same (U1 == U2 == U), the next promise's type is U. 245 | * - Otherwise, if either U1 or U2 is `void`, the next promise's type is a 246 | * `std::optional`, where X is the non-void type. 247 | * - If instead U1 and U2 are both non-void and different, the next promise's 248 | * type is a `std::variant`. 249 | * @tparam T The promise type 250 | * @tparam T_on_resolve The supplied resolve handler type 251 | * @tparam T_on_reject The supplied reject handler type 252 | */ 253 | template 254 | using chained_promise_type = common_container_t< 255 | unwrap_if_promise_t>, 256 | unwrap_if_promise_t> 257 | >; 258 | 259 | /** 260 | * @brief Determines the type of the parameter that a `.finally()` handler takes. 261 | * @details When calling `.finally()`, a single handler is called whether the 262 | * promise is resolved or rejected. Its parameter's type is determined as 263 | * follows: 264 | * 265 | * - If the promise type is void, the parameter type is a 266 | * `std::optional`. 267 | * - If the promise type T is non-void, the parameter type is a 268 | * `std::variant`. 269 | * @tparam T The promise type 270 | */ 271 | template 272 | using finally_argument_t = common_container_t; 273 | 274 | 275 | /** 276 | * @brief Helper constexpr bool to detect if a resolve handler returns void. 277 | * @tparam T The promise type 278 | * @tparam T_on_resolve The type of the resolve handler 279 | */ 280 | template 281 | static constexpr inline bool resolves_void_v = 282 | std::is_void_v>; 283 | 284 | /** 285 | * @brief Helper constexpr bool to detect if a resolve handler returns a 286 | * promise. 287 | * @tparam T The promise type 288 | * @tparam T_on_resolve The type of the resolve handler 289 | */ 290 | template 291 | static constexpr inline bool resolves_promise_v = 292 | !std::is_void_v> && 293 | is_promise_v>; 294 | 295 | /** 296 | * @brief Helper constexpr bool to detect if a resolve handler returns a 297 | * non-promise value. 298 | * @tparam T The promise type 299 | * @tparam T_on_resolve The type of the resolve handler 300 | */ 301 | template 302 | static constexpr inline bool resolves_value_v = 303 | !std::is_void_v> && 304 | !is_promise_v>; 305 | 306 | /** 307 | * @brief Helper constexpr bool to detect if a reject handler returns void. 308 | * @tparam T_on_reject The type of the reject handler 309 | */ 310 | template 311 | static constexpr inline bool rejects_void_v = 312 | std::is_void_v>; 313 | 314 | /** 315 | * @brief Helper constexpr bool to detect if a reject handler returns a promise. 316 | * @tparam T_on_reject The type of the reject handler 317 | */ 318 | template 319 | static constexpr inline bool rejects_promise_v = 320 | !std::is_void_v> && 321 | is_promise_v>; 322 | 323 | /** 324 | * @brief Helper constexpr bool to detect if a reject handler returns a 325 | * non-promise value. 326 | * @tparam T_on_reject The type of the reject handler 327 | */ 328 | template 329 | static constexpr inline bool rejects_value_v = 330 | !std::is_void_v> && 331 | !is_promise_v>; 332 | 333 | /** 334 | * @brief Yields the unwrapped type returned by a resolve handler 335 | * @tparam T The promise type 336 | * @tparam T_on_resolve The type of the resolve handler 337 | */ 338 | template 339 | using resolved_promise_value_t = 340 | unwrap_if_promise_t>; 341 | 342 | /** 343 | * @brief Yields the unwrapped type returned by a reject handler 344 | * @tparam T_on_resolve The type of the reject handler 345 | */ 346 | template 347 | using rejected_promise_value_t = 348 | unwrap_if_promise_t>; 349 | 350 | /** 351 | * @brief Store unique types into a tuple. Default clause for already unique 352 | * tuples. 353 | * @tparam T_tuple The tuple type 354 | * @tparam ... The types to filter 355 | */ 356 | template 357 | struct unique { 358 | using type = T_tuple; 359 | }; 360 | 361 | /** 362 | * @brief Stores unique types into a tuple. This clause is activated when at 363 | * least one type is yet unfiltered in a recursive manner. 364 | * @tparam ...T_values The unique stored types 365 | * @tparam T The type being currently tested 366 | * @tparam ...T_rest The rest of the unfiltered types 367 | */ 368 | template 369 | struct unique, T, T_rest...> { 370 | using type = std::conditional_t< 371 | std::disjunction_v...>, 372 | typename unique, T_rest...>::type, 373 | typename unique, T_rest...>::type 374 | >; 375 | }; 376 | 377 | /** 378 | * @brief Stores unique types into a tuple. This is the trampoline 379 | * implementation that delegates to the others. 380 | * @tparam ...T_values The values to filter 381 | */ 382 | template 383 | struct unique, std::tuple> : 384 | unique, T_values...> { }; 385 | 386 | /** 387 | * @brief Helper type that aliases `unique::type`. This defines 388 | * a tuple containing only unrepeated types of `T_values...`. 389 | * @tparam ...T_values The types to filter. 390 | */ 391 | template 392 | using unique_t = typename unique, std::tuple>::type; 393 | 394 | /** 395 | * @brief Custom implementation of `std::remove_cvref_t` 396 | * @tparam T The type to be sanitised 397 | */ 398 | template 399 | using bare_t = std::remove_reference_t>; 400 | 401 | } /* namespace juro::helpers */ 402 | 403 | #endif /* JURO_HELPERS_HPP */ -------------------------------------------------------------------------------- /juro/include/juro/promise.hpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file juro/promise.hpp 3 | * @brief Contains the definition of promise objects and imports other juro 4 | * namespaces 5 | * @author André Medeiros 6 | */ 7 | 8 | #ifndef JURO_PROMISE_HPP 9 | #define JURO_PROMISE_HPP 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include "juro/helpers.hpp" 16 | #include "juro/factories.hpp" 17 | #include "juro/compose/all.hpp" 18 | 19 | namespace juro { 20 | 21 | using namespace juro::helpers; 22 | using namespace juro::factories; 23 | using namespace juro::compose; 24 | 25 | class promise_interface { 26 | private: 27 | /** 28 | * @brief Holds the current state of the promise; Once settled, it cannot be 29 | * changed. 30 | */ 31 | promise_state state = promise_state::PENDING; 32 | 33 | /** 34 | * @brief Type-erased callback to be executed once the promise is settled. 35 | */ 36 | std::function on_settle; 37 | 38 | protected: 39 | promise_interface() noexcept = default; 40 | promise_interface(promise_state state) noexcept; 41 | promise_interface(const promise_interface &) = delete; 42 | promise_interface(promise_interface &&) noexcept = default; 43 | 44 | promise_interface &operator=(const promise_interface &) = delete; 45 | promise_interface &operator=(promise_interface &&) noexcept = default; 46 | virtual ~promise_interface() = default; 47 | 48 | void set_settle_handler(std::function &&handler) noexcept; 49 | void resolved() noexcept; 50 | void rejected(); 51 | 52 | public: 53 | /** 54 | * @brief Returns the current state of the promise. A promise is pending 55 | * when it does not hold anything yet; it is resolved when it holds a 56 | * `value_type` and is rejected when it holds a `std::exception_ptr`. 57 | * @return The current state of the promise. 58 | */ 59 | inline promise_state get_state() const noexcept { return state; } 60 | 61 | /** 62 | * @brief Returns whether the promise is pending. 63 | * @return Whether the promise is pending. 64 | */ 65 | inline bool is_pending() const noexcept { 66 | return state == promise_state::PENDING; 67 | } 68 | 69 | /** 70 | * @brief Return whether the promise is resolved. 71 | * @return Whether the promise is resolved. 72 | */ 73 | inline bool is_resolved() const noexcept { 74 | return state == promise_state::RESOLVED; 75 | } 76 | 77 | /** 78 | * @brief Return whether the promise is rejected. 79 | * @return Whether the promise is rejected. 80 | */ 81 | inline bool is_rejected() const noexcept { 82 | return state == promise_state::REJECTED; 83 | } 84 | 85 | 86 | /** 87 | * @brief Return whether the promise is either resolved or rejected. 88 | * @return Whether the promise is either resolved or rejected. 89 | */ 90 | inline bool is_settled() const noexcept { 91 | return state != promise_state::PENDING; 92 | } 93 | 94 | #ifdef JURO_TEST 95 | /** 96 | * @brief Helper function to determine if this promise has a settle handler 97 | * attached. 98 | * @return Whether there is a settle handler attached or not. 99 | */ 100 | inline bool has_handler() const noexcept { 101 | return static_cast(on_settle); 102 | } 103 | #endif /* JURO_TEST */ 104 | 105 | }; 106 | 107 | /** 108 | * @brief A promise represents a value that is not available yet. 109 | * @tparam T The type of the promised value; defaults to `void` if unspecified. 110 | */ 111 | template 112 | class promise : public promise_interface { 113 | template friend class promise; 114 | 115 | public: 116 | /** 117 | * @brief Indicates whether this is a `void` promise type or not. 118 | */ 119 | static constexpr inline bool is_void = std::is_void_v; 120 | 121 | /** 122 | * @brief The promised object type. 123 | */ 124 | using type = T; 125 | 126 | /** 127 | * @brief Defines a type suitable to hold this promise's value, no matter 128 | * its type. Maps `void` to `void_type`, every other type is mapped to 129 | * itself. 130 | */ 131 | using value_type = storage_type; 132 | 133 | /** 134 | * @brief Represents the possible values a promise can hold: a pending 135 | * promise holds an `empty_type`, a resolved promise holds a `value_type` 136 | * and a rejected promise holds an `std::exception_ptr`. 137 | */ 138 | using settle_type = 139 | std::variant; 140 | 141 | private: 142 | /** 143 | * @brief Holds the settled value or `empty_type` if the promise is pending. 144 | */ 145 | settle_type value; 146 | 147 | public: 148 | /** 149 | * @brief Constructs a pending promise. 150 | * @warning This should not be called directly; use `juro::make_pending()` 151 | * or `juro::make_promise()` instead. 152 | */ 153 | promise() = default; 154 | 155 | /** 156 | * @brief Constructs a resolved promise. 157 | * @warning This should not be called directly; use `juro::make_resolved()` 158 | * instead. 159 | * @tparam T_value The type of the value this promise is being resolved with. 160 | * @param value The value the promise is being resolved with. 161 | */ 162 | template 163 | promise(resolved_promise_tag, T_value &&value) : 164 | promise_interface { promise_state::RESOLVED }, 165 | value { std::forward(value) } 166 | { } 167 | 168 | /** 169 | * @brief Constructs a rejected promise. 170 | * @warning This should not be called directly; use `juro::make_rejected()` 171 | * instead. 172 | * @tparam T_value The type of the value this promise is being rejected with. 173 | * @param value The value the promise is being rejected with. If it is 174 | * *not* a `std::exception_ptr`, then it is wrapped into one. 175 | */ 176 | template 177 | promise(rejected_promise_tag, T_value &&value) : 178 | promise_interface { promise_state::REJECTED }, 179 | value { rejection_value(std::forward(value)) } 180 | { } 181 | 182 | promise(promise &&) = delete; 183 | promise(const promise &) = delete; 184 | ~promise() noexcept = default; 185 | 186 | promise &operator=(promise &&) = delete; 187 | promise &operator=(const promise &) = delete; 188 | 189 | 190 | /** 191 | * @brief Returns the resolved value stored in the promise. If the promise 192 | * is not resolved, will propagate a `std::bad_variant_access` exception. 193 | * @return The resolved value. 194 | */ 195 | value_type &get_value() { 196 | return std::get(value); 197 | } 198 | 199 | /** 200 | * @brief Returns the rejected value stored in the promise. If the promise 201 | * is not rejected, will propagate a `std::bad_variant_access` exception. 202 | * @return The rejected value. 203 | */ 204 | std::exception_ptr &get_error() { 205 | return std::get(value); 206 | } 207 | 208 | /** 209 | * @brief Resolves the promise with a given value. Fires the settle handler 210 | * if there is one already attached. 211 | * @tparam T_value The value type with which to settle the promise. Must be 212 | * convertible to `T`. 213 | * @param resolved_value The value with which to settle the promise. 214 | */ 215 | template 216 | void resolve(T_value &&resolved_value = {}) { 217 | static_assert( 218 | std::is_convertible_v, 219 | "Resolved value is not convertible to promise type" 220 | ); 221 | 222 | if(is_settled()) { 223 | throw promise_error { "Attempted to resolve an already settled promise" }; 224 | } 225 | 226 | value = std::forward(resolved_value); 227 | resolved(); 228 | } 229 | 230 | /** 231 | * @brief Rejects the promise with a given value. Fires the settle handler 232 | * if there is one attached, otherwise throws a `juro::promise_exception`. 233 | * @tparam T_value The value type with which to settle the promise. 234 | * @param rejected_value The value with which to settle the promise. If it 235 | * is not an `std::exception_ptr`, it will be stored into one. 236 | */ 237 | template 238 | void reject(T_value &&rejected_value = promise_error { "Promise was rejected" }) { 239 | if(is_settled()) { 240 | throw promise_error { "Attempted to reject an already settled promise" }; 241 | } 242 | 243 | value = rejection_value(std::forward(rejected_value)); 244 | rejected(); 245 | } 246 | 247 | /** 248 | * @brief Attaches a settle handler to the promise, overwriting any 249 | * previously attached one. 250 | * @tparam T_on_resolve The type of the resolve handler; should receive the 251 | * promised type as parameter, preferably as a reference. 252 | * @tparam T_on_reject The type of the reject handler; should receive an 253 | * `std::exception_ptr` as parameter, preferably as a reference. 254 | * @param on_resolve The functor to be invoked when the promise is resolved. 255 | * @param on_reject The functor to be invoked when the promise is rejected. 256 | * @return A new promise of a type that depends on the types returned by the 257 | * functors provided. 258 | * @see `juro::helpers::chained_promise_type` 259 | */ 260 | template 261 | auto then(T_on_resolve &&on_resolve, T_on_reject &&on_reject) { 262 | assert_resolve_invocable(); 263 | assert_reject_invocable(); 264 | 265 | using next_value_type = 266 | chained_promise_type; 267 | 268 | return make_promise([&] (auto &next_promise) { 269 | set_settle_handler([ 270 | this, 271 | next_promise, 272 | on_resolve = std::forward(on_resolve), 273 | on_reject = std::forward(on_reject) 274 | ] { 275 | try { 276 | if(is_resolved()) { 277 | handle_resolve(on_resolve, next_promise); 278 | } else if(is_rejected()) { 279 | handle_reject(on_reject, next_promise); 280 | } 281 | } catch(...) { 282 | next_promise->reject(std::current_exception()); 283 | } 284 | }); 285 | }); 286 | } 287 | 288 | /** 289 | * @brief Attaches a resolve handler to the promise, overwriting any 290 | * previously attached one. In case of rejection, the error will be 291 | * propagated down the promise chain. 292 | * @tparam T_on_resolve The type of the resolve handler; should receive the 293 | * promise type as parameter, preferably by reference. 294 | * @param on_resolve The functor to be invoked when the promise is resolved. 295 | * @return A new promise of a type that depends on the type returned by the 296 | * functor provided. 297 | * @see `juro::helpers::chained_promise_type` 298 | */ 299 | template 300 | inline auto then(T_on_resolve &&on_resolve) { 301 | return then( 302 | std::forward(on_resolve), 303 | [] (auto &error) -> resolve_result_t { std::rethrow_exception(error); } 304 | ); 305 | } 306 | 307 | /** 308 | * @brief Attaches a reject handler to the promise, overwriting any 309 | * previously attached one. If resolved, the value will be passed down 310 | * along the promise chain. 311 | * @tparam T_on_reject The type of the reject handler; should receive a 312 | * `std::exception_ptr` as parameter, preferably by reference. 313 | * @param on_reject The functor to be invoked when the promise is rejected. 314 | * @return A new promise of a type that depends on the type returned by the 315 | * functor provided. 316 | * @see `juro::helpers::chained_promise_type` 317 | */ 318 | template 319 | inline auto rescue(T_on_reject &&on_reject) { 320 | if constexpr(is_void) { 321 | return then([] () noexcept {}, std::forward(on_reject)); 322 | } else { 323 | return then( 324 | [] (auto &value) noexcept { return value; }, 325 | std::forward(on_reject) 326 | ); 327 | } 328 | } 329 | 330 | /** 331 | * @brief Attaches a settle handler to the promise, overwriting any 332 | * previously attached one. The handler will be invoked upon settling 333 | * whether the promise is resolved or rejected. 334 | * @tparam T_on_settle The type of the settle handler; should receive as 335 | * parameter a `juro::helpers::finally_argument_t`, preferably by reference. 336 | * @return A new promise of a type that depends on the type returned by the 337 | * functor provided. 338 | * @see `juro::helpers::chained_promise_type` 339 | * @see `juro::helpers::finally_argument_t` 340 | */ 341 | template 342 | inline auto finally(T_on_settle &&on_settle) { 343 | assert_settle_invocable(); 344 | 345 | if constexpr(is_void) { 346 | return then( 347 | [=] { return on_settle(std::nullopt); }, 348 | std::forward(on_settle) 349 | ); 350 | } else { 351 | return then(on_settle, on_settle); 352 | } 353 | } 354 | 355 | private: 356 | /** 357 | * @brief Asserts that a particular callable type is suitable to be invoked 358 | * when the promise is resolved. 359 | * @tparam T_on_resolve The callable type. 360 | */ 361 | template 362 | static inline void assert_resolve_invocable() noexcept { 363 | static_assert( 364 | (is_void && std::is_invocable_v) || 365 | std::is_invocable_v, 366 | "Resolve handler has an incompatible signature." 367 | ); 368 | } 369 | 370 | /** 371 | * @brief Asserts that a particular callable type is suitable to be invoked 372 | * when the promise is rejected. 373 | * @tparam T_on_resolve The callable type. 374 | */ 375 | template 376 | static inline void assert_reject_invocable() noexcept { 377 | static_assert( 378 | std::is_invocable_v, 379 | "Reject handler has an incompatible signature." 380 | ); 381 | } 382 | 383 | /** 384 | * @brief Asserts that a particular callable type is suitable to be invoked 385 | * when the promise is settled. 386 | * @tparam T_on_resolve The callable type. 387 | */ 388 | template 389 | static inline void assert_settle_invocable() noexcept { 390 | static_assert( 391 | std::is_invocable_v &>, 392 | "Settle handler has an incompatible signature." 393 | ); 394 | } 395 | 396 | /** 397 | * @brief Handles promise resolution, calling the resolve handler and 398 | * resolving the chained promise. 399 | * @tparam T_on_resolve The type of the resolve handler 400 | * @tparam T_next_promise The type of the chained promise 401 | * @param on_resolve The resolve handler to be invoked 402 | * @param next_promise The chained promise 403 | */ 404 | template 405 | void handle_resolve(T_on_resolve &on_resolve, T_next_promise &next_promise) { 406 | if constexpr(is_void) { 407 | if constexpr(resolves_void_v) { 408 | on_resolve(); 409 | next_promise->resolve(); 410 | } 411 | if constexpr(resolves_value_v) { 412 | next_promise->resolve(on_resolve()); 413 | } 414 | if constexpr(resolves_promise_v) { 415 | on_resolve()->pipe(next_promise); 416 | } 417 | } else { 418 | if constexpr(resolves_void_v) { 419 | on_resolve(std::get(value)); 420 | next_promise->resolve(); 421 | } 422 | if constexpr(resolves_value_v) { 423 | next_promise->resolve(on_resolve(std::get(value))); 424 | } 425 | if constexpr(resolves_promise_v) { 426 | on_resolve(std::get(value))->pipe(next_promise); 427 | } 428 | 429 | } 430 | } 431 | 432 | /** 433 | * @brief Handler promise rejection, calling the reject handler and 434 | * resolving the chained promise. 435 | * @tparam T_on_reject The type of the reject handler 436 | * @tparam T_next_promise The type of the chained promise 437 | * @param on_reject The reject handler to be invoked 438 | * @param next_promise The chained promise 439 | */ 440 | template 441 | void handle_reject(T_on_reject &&on_reject, T_next_promise &next_promise) { 442 | if constexpr(rejects_void_v) { 443 | on_reject(std::get(value)); 444 | next_promise->resolve(); 445 | } 446 | if constexpr(rejects_value_v) { 447 | auto &rejected_value = std::get(value); 448 | next_promise->resolve(on_reject(rejected_value)); 449 | } 450 | if constexpr(rejects_promise_v) { 451 | auto &rejected_value = std::get(value); 452 | on_reject(rejected_value)->pipe(next_promise); 453 | 454 | } 455 | } 456 | 457 | /** 458 | * @brief Prepares a value for rejection: wraps the supplied value into a 459 | * `std::exception_ptr` unless it already is one, in which case it is simply 460 | * returned. 461 | * @tparam T_value The type of value to be prepared 462 | * @param value The value to be prepared for rejection 463 | * @return A `std::exception_ptr` suitable for promise rejection. 464 | */ 465 | template 466 | inline auto rejection_value(T_value &&value) { 467 | using bare_type = std::remove_cv_t>; 468 | if constexpr(std::is_same_v) { 469 | return std::forward(value); 470 | } else { 471 | return std::make_exception_ptr(std::forward(value)); 472 | } 473 | } 474 | 475 | /** 476 | * @brief Pipes a promise into another: when the current promise is settled, 477 | * the next will be too with the same state and value. 478 | * @tparam T_next_promise The target promise type 479 | * @param next_promise the The target promise 480 | */ 481 | template 482 | inline void pipe(T_next_promise &&next_promise) { 483 | if constexpr(is_void) { 484 | then( 485 | [=] { next_promise->resolve(); }, 486 | [=] (auto &error) { next_promise->reject(std::move(error)); } 487 | ); 488 | } else { 489 | then( 490 | [=] (auto &value) { next_promise->resolve(std::move(value)); }, 491 | [=] (auto &error) { next_promise->reject(std::move(error)); } 492 | ); 493 | } 494 | } 495 | 496 | #ifdef JURO_TEST 497 | public: 498 | /** 499 | * @brief Helper function to determine if this promise holds a determinate 500 | * value type. 501 | * @tparam T_value The type being evaluated. 502 | * @return Whether the promise's value container holds a value of type 503 | * `T_value` or not. 504 | */ 505 | template 506 | inline bool holds_value() const noexcept { 507 | return std::holds_alternative(value); 508 | } 509 | 510 | /** 511 | * @brief Helper function to determine if this promise holds no meaningful 512 | * value, i.e., an `empty_type`. 513 | * @return Whether this promise's value container holds a value of type 514 | * `empty_type` or not. 515 | */ 516 | inline bool is_empty() const noexcept { 517 | return std::holds_alternative(value); 518 | } 519 | 520 | #endif /* JURO_TEST */ 521 | }; 522 | 523 | } /* namespace juro */ 524 | 525 | #endif /* JURO_PROMISE_HPP */ -------------------------------------------------------------------------------- /fugax/README.md: -------------------------------------------------------------------------------- 1 | # Fugax 2 | 3 | Fugax is an efficient, modern, ergonomic and portable event loop written in C++17. 4 | 5 | ## Table of Contents 6 | 7 | 8 | * [Fugax](#fugax) 9 | * [Table of Contents](#table-of-contents) 10 | * [Features](#features) 11 | * [Quick demo](#quick-demo) 12 | * [The event loop](#the-event-loop) 13 | * [Runloops and the execution counter](#runloops-and-the-execution-counter) 14 | * [Bare metal](#bare-metal) 15 | * [RTOS / Desktop](#rtos--desktop) 16 | * [Modes of operation](#modes-of-operation) 17 | * [Spinning mode](#spinning-mode) 18 | * [Ticking mode](#ticking-mode) 19 | * [Event scheduling](#event-scheduling) 20 | * [Main schedule function](#main-schedule-function) 21 | * [Event handler](#event-handler) 22 | * [Schedule policy](#schedule-policy) 23 | * [Delay](#delay) 24 | * [Other schedule overloads and functions](#other-schedule-overloads-and-functions) 25 | * [Immediate scheduling](#immediate-scheduling) 26 | * [Delayed scheduling](#delayed-scheduling) 27 | * [Conditionally recurring scheduling](#conditionally-recurring-scheduling) 28 | * [Continuous execution scheduling](#continuous-execution-scheduling) 29 | * [Event listeners and event cancellation](#event-listeners-and-event-cancellation) 30 | * [Event guards](#event-guards) 31 | * [Other non-core functionality](#other-non-core-functionality) 32 | * [Throttling and debouncing](#throttling-and-debouncing) 33 | * [Throttlers](#throttlers) 34 | * [Debouncers](#debouncers) 35 | * [Juro integration](#juro-integration) 36 | * [Waiting](#waiting) 37 | * [Asynchronous timeouts](#asynchronous-timeouts) 38 | 39 | 40 | ## Features 41 | 42 | - Light, sleek and fast 43 | - Ergonomic and flexible API 44 | - Completely portable 45 | - Fit for embedded designs and desktop applications alike 46 | 47 | ## Quick demo 48 | 49 | ```C++ 50 | #include 51 | #include 52 | #include 53 | 54 | static std::atomic milliseconds = 0; 55 | 56 | // Interrupt or thread kicking in every 1ms 57 | void my_timer_interrupt() { 58 | milliseconds++; 59 | } 60 | 61 | void print_counter() { 62 | std::cout << "counter: " << milliseconds << "ms" << std::endl; 63 | } 64 | 65 | int main(int argc, const char *argv[]) { 66 | fugax::event_loop loop; 67 | bool done; 68 | 69 | // Schedules any functor to 100ms into the future 70 | loop.schedule(1000, [] { 71 | print_counter(); 72 | done = true; 73 | }); 74 | 75 | // Can schedule recurring tasks also 76 | loop.schedule(499, true, [] { 77 | print_counter(); 78 | }); 79 | 80 | print_counter(); 81 | 82 | // Spins the loop, ensuring events are processes ASAP 83 | while(!done) { 84 | loop.process(milliseconds); 85 | } 86 | 87 | return 0; 88 | } 89 | ``` 90 | 91 | ``` 92 | prints immediately 93 | > counter: 0ms 94 | 95 | prints after 499 milliseconds 96 | > counter: 499ms 97 | 98 | prints after 998 milliseconds 99 | > counter: 998ms 100 | 101 | prints after one second 102 | > counter: 1000ms 103 | 104 | then exits with status 0 105 | ``` 106 | 107 | ## The event loop 108 | 109 | The central piece of Fugax is its event loop. It is a data structure that maintains a schedule of 110 | events, represented as functors, indexed by their due time. 111 | 112 | Upon continuous stimulation with a constantly increasing execution time counter, it can 113 | coordinate execution of tasks, serving as both source and destination of asynchrony to an 114 | application. 115 | 116 | ### Runloops and the execution counter 117 | 118 | The event loop must be continuously stimulated to process any due events. This is done through the 119 | `.process()` member function, that takes a `fugax::time_type` as parameter: 120 | 121 | ```C++ 122 | // This gets incremented every 1ms by an ISR or thread 123 | static std::atomic milliseconds = 0; 124 | 125 | // In the main function 126 | while(true) { 127 | loop.process(milliseconds); 128 | } 129 | ``` 130 | 131 | Each call to `.process()` is called a **runloop**. It comprehends all necessary housekeeping, such 132 | as collecting due events, executing them and rescheduling any recurring ones. Its sole parameter 133 | is of an integral type and should be monotonically increased over time: the **execution counter**. 134 | 135 | Because the execution counter is external to the event loop, as long as there is a way to keep 136 | track of time it can be easily integrated to many kinds of systems. 137 | 138 | #### Bare metal 139 | 140 | When running on a bare metal system, having a 1kHz interruption increment the counter is fairly 141 | easy. On a multitude of system it is also possible to have a 1kHz hardware counter that can be 142 | also directly fed to the loop. It doesn't matter. 143 | 144 | ```C++ 145 | // 1kHz interruption 146 | void my_timer_isr() { 147 | milliseconds++; 148 | } 149 | ``` 150 | 151 | #### RTOS / Desktop 152 | 153 | On a RTOS, a worker thread can be solely responsible for incrementing the counter. Likewise, 154 | the main thread can simply keep reading the system clock and calculating the delta between calls 155 | to `.process()`, although this is unnecessary onerous. 156 | 157 | ```C++ 158 | #include 159 | #include 160 | 161 | using namespace std::chrono_literals; 162 | 163 | // Worker thread responsible for keeping the execution counter running 164 | std::thread clock_worker { [] { 165 | 166 | // Time point when the application started running 167 | const auto initial = std::chrono::steady_clock::now(); 168 | 169 | while(true) { 170 | // Every 1ms or so, wake up and update the timer 171 | std::this_thread::sleep_for(1ms); 172 | 173 | // Because sleeping is not reliable as a source of time counting, 174 | // we calculate the time difference from the initial counter on 175 | // every iteration. This makes up for the interval inaccuracy. 176 | const auto now = std::chrono::steady_clock::now(); 177 | const auto delta = 178 | std::chrono::duration_cast(now - initial); 179 | milliseconds = delta.count(); 180 | } 181 | } }; 182 | ``` 183 | ### Modes of operation 184 | 185 | The event loop can be operated on both ticking and spinning modes. These modes will affect major 186 | aspects of the application, namely: fixed schedule latency and computing time (which translates 187 | into power consumption). 188 | 189 | #### Spinning mode 190 | 191 | So far only the spinning mode has been introduced: threads or interruptions are responsible for 192 | updating the execution counter and, in the main thread, the event loop *spins* to process 193 | events as they arrive: 194 | 195 | ```C++ 196 | while(true) { 197 | loop.process(milliseconds); 198 | } 199 | ``` 200 | 201 | This ensures a minimum fixed schedule latency -- the time between the scheduling of an event for 202 | immediate execution (basically, a timer with zero delay) and its effective execution is simply the 203 | delay between finishing the current runloop and initiating the next one, when the event's task 204 | will be executed, plus any time spent running and managing other tasks. 205 | 206 | However, this means that the loop will keep spinning even if there is no work to be done at all. 207 | On many systems this is acceptable, but on those where it is not, ticking mode can be employed. 208 | 209 | #### Ticking mode 210 | 211 | When running on ticking mode, instead of using a thread or an interruption as a side worker, 212 | whose purpose is solely to increment our execution counter, we move the whole runloop into the 213 | worker. This means that more events will accumulate over time and be processed in batches, what 214 | can be more forgiving on computing time, but can accumulate latency and deteriorate time precision. 215 | 216 | ```C++ 217 | #include 218 | #include 219 | 220 | static fugax::event_loop loop; 221 | static std::atomic counter = 0; 222 | 223 | // 1kHz interruption, assume the timer keeps running on sleep and 224 | // the interruption wakes the device up 225 | void my_timer_isr() { 226 | loop.process(++counter); 227 | } 228 | 229 | int main(int argc, const char *argv[]) { 230 | // all configuration and stuff 231 | 232 | // nothing else to do with main, put the device to sleep 233 | my_device_sleep(); 234 | 235 | return 0; 236 | } 237 | ``` 238 | 239 | In this setup, the device will spend most time sleeping unless there is something heavy to process. 240 | If there are other sources of asynchrony in the system, such as other interruptions that can wake 241 | the device up and schedule tasks in the loop, there is no fixed schedule latency for these tasks, 242 | but their *maximum* schedule latency is the interruption interval plus housekeeping time. 243 | 244 | When scheduling an event from within a runloop -- that is, by any code that is called by the event 245 | loop --, the *minimum* schedule latency is the interruption interval plus housekeeping time. 246 | 247 | This same setup can be accomplished with a RTOS by simply putting the main thread to sleep: 248 | 249 | ```C++ 250 | #include 251 | #include 252 | #include 253 | 254 | using namespace std::chrono_literals; 255 | 256 | int main(int argc, const char *argv[]) { 257 | fugax::event_loop loop; 258 | 259 | const auto initial = std::chrono::steady_clock::now(); 260 | while(true) { 261 | std::this_thread::sleep_for(1ms); 262 | 263 | const auto now = std::chrono::steady_clock::now(); 264 | const auto delta = 265 | std::chrono::duration_cast(now - initial); 266 | loop.process(delta.count()); 267 | } 268 | 269 | return 0; 270 | } 271 | ``` 272 | 273 | ## Event scheduling 274 | 275 | Once a loop is running properly, it will start processing due events as they are scheduled. This is 276 | accomplished mainly through the `.schedule()` function, which comes with various overloads for 277 | ease of use. 278 | 279 | ### Main schedule function 280 | 281 | The main and most flexible overload of `.schedule()` takes as parameters a delay, a schedule policy 282 | and an event handler, a task functor that will be invoked when the event's due time arrives: 283 | 284 | ```C++ 285 | fugax::event_listener 286 | schedule(fugax::time_type delay, fugax::schedule_policy policy, fugax::event_handler functor); 287 | ``` 288 | 289 | #### Event handler 290 | 291 | The event handler keeps a reference to the actual code that will be run when the due time arrives 292 | and is always a mandatory parameter. 293 | It can be any callable and, optionally, take a `fugax::event` as parameter, which can be used to 294 | cancel the event from inside it. 295 | 296 | ```C++ 297 | // This can be an event handler: 298 | auto handler_1 = [] { do_something(); }; 299 | 300 | // this can also be an event handler: 301 | static void handler_2(fugax::event &event) { 302 | do_something(); 303 | } 304 | ``` 305 | 306 | Other than its signature, the only requirement of the provided functor is that it is 307 | move-constructible. 308 | 309 | #### Schedule policy 310 | 311 | The schedule policy is an `enum class` that determines how this event is to be scheduled in the 312 | event loop. Its possible values are: 313 | 314 | - `immediate`: The task will be executed as soon as possible, on the next runloop 315 | - `delayed`: The task will be executed some time into the future 316 | - `recurring_immediate`: The task will be executed periodically; its first execution will 317 | occur on the next runloop 318 | - `recurring_delayed`: The task will be executed periodically; its first execution will occur 319 | after the informed delay has passed 320 | - `always`: The task will be executed on **every** runloop until it is cancelled 321 | 322 | #### Delay 323 | 324 | The delay parameter represents the minimum time that must pass between the scheduling of an 325 | event and its processing. 326 | It is always in system units (so if the event loop is counting milliseconds, so is the delay) and 327 | is ignored when scheduling with `immediate` or `always` policies. 328 | 329 | ### Other schedule overloads and functions 330 | 331 | There are other overloads of the `.schedule()` function that can be used to more easily schedule 332 | events with specific policies: 333 | 334 | #### Immediate scheduling 335 | ```C++ 336 | // same as schedule(0, fugax::schedule_policy::immediate, functor) 337 | fugax::event_listener schedule(fugax::event_handler functor); 338 | ``` 339 | 340 | #### Delayed scheduling 341 | ```C++ 342 | // same as schedule(delay, fugax::schedule_policy::delayed, functor) 343 | fugax::event_listener 344 | schedule(fugax::time_type delay, fugax::event_handler functor); 345 | ``` 346 | 347 | #### Conditionally recurring scheduling 348 | ```C++ 349 | // if `recurring`, same as: 350 | // schedule(delay, fugax::schedule_policy::recurring_delayed, functor) 351 | // else, same as: 352 | // schedule(delay, fugax::schedule_policy::delayed, functor) 353 | fugax::event_listener 354 | schedule(fugax::time_type delay, bool recurring, fugax::event_handler functor); 355 | ``` 356 | 357 | #### Continuous execution scheduling 358 | ```C++ 359 | // same as schedule(0, fugax::schedule_policy::always, functor) 360 | fugax::event_listener always(fugax::event_handler functor); 361 | ``` 362 | 363 | ## Event listeners and event cancellation 364 | 365 | Every scheduling function returns a `fugax::event_listener`, which is merely an alias for a 366 | `std::weak_ptr`. This listener can be used to cancel the event: 367 | 368 | ```C++ 369 | // schedules the functor to be executed 100ms in the future 370 | auto listener = loop.schedule(100, [] { do_something(); }); 371 | 372 | if(auto event = listener.lock()) { 373 | event->cancel(); 374 | } 375 | ``` 376 | 377 | Because events are destroyed when they are not needed any more, such as after any non-recurring 378 | events are executed, locking the listener will fail in these cases and no illegal memory will 379 | ever be accessed. 380 | 381 | Besides cancelling the event from outside, an event's task functor may receive an optional 382 | `fugax::event &` parameter that directly exposes the event object, so it can be cancelled from 383 | within: 384 | 385 | ```C++ 386 | // schedules a recurring functor to be executed every 1000ms 387 | loop.schedule(1000, true, [&] (auto &event) { 388 | 389 | // the event must not run any more; cancel it and return 390 | if(some_critical_async_assumption_has_changed) { 391 | event.cancel(); 392 | return; 393 | } 394 | 395 | do_something(); 396 | }); 397 | ``` 398 | 399 | When calling `.cancel()`, an event is marked as invalid and, when its due time arrives, instead of 400 | being processed, it will be simply discarded. 401 | 402 | ### Event guards 403 | 404 | Event guards are RAII containers that manages event listeners, attempting to cancel their source 405 | event when destroyed. They are very useful for consistently ensuring no asynchronous events that 406 | target a dynamic object will ever be fired after the object is destroyed. 407 | 408 | They can be created and assigned directly from `fugax::event_listeners` for maximum easiness. 409 | 410 | ```C++ 411 | class my_object { 412 | std::string name; 413 | fugax::event_guard guard; 414 | 415 | public: 416 | my_object(fugax::event_loop &loop, std::string name) : 417 | name { name }, 418 | guard { 419 | loop.schedule(1000, [this] { greet(); }) 420 | } 421 | { } 422 | 423 | void greet() { 424 | std::cout << "Ave, " << name << std::endl; 425 | } 426 | }; 427 | ``` 428 | 429 | In this snippet, when an instance of `my_object` is created, it will schedule an event to be run 430 | after one second: 431 | 432 | ```C++ 433 | fugax::event_loop loop; 434 | my_object greeter { loop, "Caesar" }; 435 | 436 | while(true) { 437 | loop.process(milliseconds); 438 | } 439 | ``` 440 | 441 | ``` 442 | after one second spinning, will print 443 | > Ave, Caesar 444 | ``` 445 | 446 | If the duration of `greeter` was, instead, dynamic and actually shorter than the event's delay, 447 | the event guard contained in the greeter would automatically cancel the event upon destruction, 448 | ensuring no dangling pointers are accessed: 449 | 450 | ```C++ 451 | fugax::event_loop loop; 452 | auto *greeter = new my_object { loop, "Caesar" }; 453 | 454 | loop.schedule(500, [greeter] { 455 | delete greeter; 456 | }); 457 | 458 | while(true) { 459 | loop.process(milliseconds); 460 | } 461 | ``` 462 | 463 | Because the greeter is deleted after 500 milliseconds, the event it scheduled is automatically 464 | cancelled and won't ever be processed. 465 | 466 | ## Other non-core functionality 467 | 468 | ### Throttling and debouncing 469 | 470 | The event loop provide special template functions that act as throttlers and debouncers of 471 | wrapped functors. 472 | 473 | #### Throttlers 474 | 475 | ```C++ 476 | template 477 | auto throttle(time_type delay, T_functor &&functor); 478 | ``` 479 | 480 | This template function gets a functor of type `T_functor` that accepts as parameters the types 481 | `T_args...` and returns a throttled version of the functor. This returned functor has the same 482 | signature as the wrapped one and, even upon frequent invocation, will only invoke the initial 483 | functor every `delay` units of time; any other calls during this suspension interval will be 484 | silently swallowed. 485 | 486 | ```C++ 487 | fugax::event_loop loop; 488 | 489 | auto throttled = loop.throttle(100, [] { 490 | std::cout << "running" << std::endl; 491 | }); 492 | 493 | throttled(); // prints "running" 494 | throttled(); // nothing happens 495 | throttled(); // nothing happens 496 | 497 | loop.process(100); 498 | throttled(); // prints "running" 499 | throttled(); // nothing happens 500 | ``` 501 | 502 | #### Debouncers 503 | 504 | ```C++ 505 | template 506 | auto debounce(time_type delay, T_functor &&functor); 507 | ``` 508 | 509 | Similarly to the `.throttle()` function, the `.debounce()` template function receives a delay and a 510 | functor as parameters and returns a wrapping functor with the same signature. When called, instead 511 | of immediately invoking its wrapped functor, the debounced version scheduled an event with `delay` 512 | units of time. 513 | 514 | If the debounced functor is not invoked until this event is due, the wrapped functor gets called. 515 | Otherwise, the event is rescheduled for execution after `delay` units of time from the invocation 516 | occasion. This means that the initial functor will only get called after `delay` has passed without 517 | the debounced version being invoked. 518 | 519 | The debounced value gets stores inside the returned functor object and is updated each time it is 520 | invoked, overriding the last value. Because of this, when the initial functor is finally called, 521 | it will receive the **last** debounced value every time. 522 | 523 | ```C++ 524 | fugax::event_loop loop; 525 | 526 | auto debounced = loop.debounce(100, [] (int value) { 527 | std::cout << "got " << value << std::endl; 528 | }); 529 | 530 | debounced(1); // event is scheduled 531 | debounced(2); // event is rescheduled 532 | debounced(3); // event is rescheduled 533 | 534 | loop.process(100); // will print "got 3" 535 | 536 | debounced(4); // event is scheduled 537 | debounced(5); // event is rescheduled 538 | ``` 539 | 540 | ### Juro integration 541 | 542 | The event loop can also be used to construct time-dependent promises that can be useful 543 | primitives in complex asynchronous operations. 544 | 545 | #### Waiting 546 | ```C++ 547 | juro::promise_ptr wait(time_type delay); 548 | ```` 549 | 550 | Returns a promise that resolves with an instance of the tag-type `fugax::timeout` after 551 | `delay` units of time and never rejects. 552 | 553 | #### Asynchronous timeouts 554 | ```C++ 555 | template 556 | auto timeout(time_type delay, const juro::promise_ptr &promise); 557 | ``` 558 | 559 | Returns a race promise between the supplied promise and a `.wait()` invocation. When any of 560 | the promises resolves, the race promise will be resolved with that same value; if the 561 | supplied promise rejects, the race promise will be rejected with that same error. 562 | 563 | In the attached resolution handler, a timeout situation can be identified by STL utilities. 564 | 565 | ```C++ 566 | fugax::event_loop loop; 567 | 568 | loop.timeout(100, juro::make_promise([&] (auto &promise) { 569 | loop.schedule(50, [promise] { 570 | promise->resolve("resolved"); 571 | }); 572 | })) 573 | ->then([] (auto &result) { 574 | if(std::holds_alternative(result)) { 575 | // timeout occurred 576 | } else { 577 | auto &string = std::get(result); // "resolved" 578 | } 579 | }); 580 | ``` 581 | 582 | In the previous snippet, as the promise resolution happens before the timeout (50ms vs 100ms), 583 | the result provided to the resolution handler contains a `std::string`. If the resolution was 584 | scheduled further than the timeout, the result would contain a `fugax::timeout`. 585 | 586 | A timeout race can also be directly started through the other `.timeout()` overload: 587 | 588 | ```C++ 589 | template 590 | auto timeout(time_type delay, const T_launcher &launcher); 591 | ``` 592 | 593 | Instead of receiving a promise as parameter, it receives a promise *launcher* and forwards it to 594 | `juro::make_promise`, so a promise can be created inline. 595 | 596 | ```C++ 597 | loop.timeout(100, [&] (auto &promise) { 598 | loop.schedule(50, [promise] { 599 | promise->resolve("resolved"); 600 | }); 601 | }); 602 | ``` -------------------------------------------------------------------------------- /test/src/fugax/test.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @file fugax/test/src/test.cpp 3 | * @brief Fugax test routines 4 | * @author André Medeiros 5 | * @date 27/06/23 6 | * @copyright 2023 (C) André Medeiros 7 | **/ 8 | 9 | 10 | #include 11 | #include 12 | 13 | #include "test/fugax/helpers.hpp" 14 | 15 | using namespace fugax::test::helpers; 16 | using namespace std::string_literals; 17 | using namespace utils::types; 18 | 19 | SCENARIO("an event loop can be created", "[fugax]") { 20 | GIVEN("an event loop constructor") { 21 | WHEN("it is invoked") { 22 | auto result = attempt([] { 23 | [[maybe_unused]] fugax::event_loop loop; 24 | }); 25 | 26 | THEN("it should not throw anything") { 27 | REQUIRE_FALSE(result.has_error()); 28 | } 29 | } 30 | } 31 | } 32 | 33 | SCENARIO("an event loop can be operated accordingly", "[fugax]") { 34 | GIVEN("an event loop") { 35 | fugax::event_loop loop; 36 | 37 | WHEN("a task is scheduled for immediate execution") { 38 | bool task_executed = false; 39 | 40 | schedule_for_test([&] { 41 | return loop.schedule([&] { task_executed = true; }); 42 | }, [&] (auto &listener) { 43 | 44 | THEN("the task must not have been executed") { 45 | REQUIRE_FALSE(task_executed); 46 | } 47 | 48 | AND_WHEN("the event loop is stimulated to process scheduled events") { 49 | auto process_result = attempt([&] { 50 | loop.process(0); 51 | }); 52 | 53 | THEN("no exception should have been thrown") { 54 | REQUIRE_FALSE(process_result.has_error()); 55 | } 56 | 57 | THEN("the task must have been executed") { 58 | REQUIRE(task_executed); 59 | } 60 | 61 | THEN("the scheduled event must have been destroyed") { 62 | REQUIRE(listener.expired()); 63 | } 64 | } 65 | }); 66 | } 67 | 68 | WHEN("a task is scheduled for future execution") { 69 | bool task_executed = false; 70 | 71 | schedule_for_test([&] { 72 | return loop.schedule(100, [&] { task_executed = true; }); 73 | }, [&] (auto &listener) { 74 | 75 | THEN("the task must not have been executed") { 76 | REQUIRE_FALSE(task_executed); 77 | } 78 | 79 | AND_WHEN( 80 | "the event loop is stimulated with an updated time value, "\ 81 | "smaller than the task delay" 82 | ) { 83 | loop.process(90); 84 | 85 | THEN("the task must not have been executed") { 86 | REQUIRE_FALSE(task_executed); 87 | } 88 | 89 | THEN("the scheduled event must still exist") { 90 | REQUIRE_FALSE(listener.expired()); 91 | } 92 | } 93 | 94 | AND_WHEN( 95 | "the event loop is stimulated with an updated time value, "\ 96 | "greater than the task delay" 97 | ) { 98 | loop.process(110); 99 | 100 | THEN("the task must have been executed") { 101 | REQUIRE(task_executed); 102 | } 103 | 104 | THEN("the scheduled event must have been destroyed") { 105 | REQUIRE(listener.expired()); 106 | } 107 | } 108 | 109 | AND_WHEN("the task is rescheduled to a further time value") { 110 | auto reschedule_result = attempt([&] { 111 | listener.lock()->reschedule(200); 112 | }); 113 | 114 | THEN("not exception must have been thrown") { 115 | REQUIRE_FALSE(reschedule_result.has_error()); 116 | } 117 | 118 | AND_WHEN( 119 | "the event loop is stimulated with an updated time value, "\ 120 | "greater than the initial task delay and smaller than the "\ 121 | "rescheduled delay" 122 | ) { 123 | loop.process(110); 124 | 125 | THEN("the task must not have been executed") { 126 | REQUIRE_FALSE(task_executed); 127 | } 128 | 129 | THEN("the event must still exist") { 130 | REQUIRE_FALSE(listener.expired()); 131 | } 132 | 133 | AND_WHEN( 134 | "the event loop is stimulated with an updated time value, "\ 135 | "greater than the rescheduled delay" 136 | ) { 137 | loop.process(210); 138 | 139 | THEN("the task must have been executed") { 140 | REQUIRE(task_executed); 141 | } 142 | 143 | THEN("the event must have been destroyed") { 144 | REQUIRE(listener.expired()); 145 | } 146 | } 147 | } 148 | } 149 | 150 | AND_WHEN("the task is cancelled") { 151 | auto cancel_result = attempt([&] { 152 | listener.lock()->cancel(); 153 | }); 154 | 155 | THEN("no exception must have been thrown") { 156 | REQUIRE_FALSE(cancel_result.has_error()); 157 | } 158 | 159 | AND_WHEN( 160 | "the event loop is stimulated with an updated time value, "\ 161 | "greater than the task delay" 162 | ) { 163 | loop.process(110); 164 | 165 | THEN("the task must still not have been executed") { 166 | REQUIRE_FALSE(task_executed); 167 | } 168 | 169 | THEN("the scheduled event must have been destroyed") { 170 | REQUIRE(listener.expired()); 171 | } 172 | } 173 | } 174 | }); 175 | } 176 | 177 | WHEN("a task is scheduled for recurring execution") { 178 | const auto interval = 10; 179 | int execution_count = 0; 180 | schedule_for_test([&] { 181 | return loop.schedule(interval, true,[&] { execution_count++; }); 182 | }, [&] (auto &listener) { 183 | 184 | THEN("the task must not have been executed") { 185 | REQUIRE(execution_count == 0); 186 | } 187 | 188 | AND_WHEN("the task interval is elapsed") { 189 | loop.process(interval); 190 | 191 | THEN("the task must have been executed once") { 192 | REQUIRE(execution_count == 1); 193 | 194 | AND_THEN("the event must still exist") { 195 | REQUIRE_FALSE(listener.expired()); 196 | } 197 | } 198 | 199 | AND_WHEN("the task interval is elapsed again") { 200 | loop.process(2 * interval); 201 | 202 | THEN("the task must have been executed twice") { 203 | REQUIRE(execution_count == 2); 204 | 205 | AND_THEN("the event must still exist") { 206 | REQUIRE_FALSE(listener.expired()); 207 | } 208 | } 209 | } 210 | } 211 | }); 212 | } 213 | 214 | WHEN("a task is scheduled for immediate recurring execution") { 215 | const auto interval = 10; 216 | int execution_count = 0; 217 | schedule_for_test([&] { 218 | return loop.schedule( 219 | interval, 220 | fugax::schedule_policy::recurring_immediate, 221 | [&] { execution_count++; } 222 | ); 223 | }, [&] (auto &listener) { 224 | 225 | THEN("the task must not have been executed") { 226 | REQUIRE(execution_count == 0); 227 | } 228 | 229 | AND_WHEN("the event loop is stimulated to process immediate events") { 230 | loop.process(0); 231 | 232 | THEN("the task must have been executed once") { 233 | REQUIRE(execution_count == 1); 234 | 235 | AND_THEN("the event must still exist") { 236 | REQUIRE_FALSE(listener.expired()); 237 | } 238 | } 239 | 240 | AND_WHEN("the task interval is elapsed") { 241 | loop.process(interval); 242 | 243 | THEN("the task must have been executed twice") { 244 | REQUIRE(execution_count == 2); 245 | 246 | AND_THEN("the event must still exist") { 247 | REQUIRE_FALSE(listener.expired()); 248 | } 249 | } 250 | 251 | AND_WHEN("the task interval is elapsed again") { 252 | loop.process(2 * interval); 253 | 254 | THEN("the task must have been executed three times") { 255 | REQUIRE(execution_count == 3); 256 | 257 | AND_THEN("the event must still exist") { 258 | REQUIRE_FALSE(listener.expired()); 259 | } 260 | } 261 | } 262 | } 263 | } 264 | }); 265 | } 266 | 267 | WHEN("a task is scheduled for continuous execution") { 268 | int execution_count = 0; 269 | fugax::time_type clock = 0; 270 | 271 | schedule_for_test([&] { 272 | return loop.always([&] { execution_count++; }); 273 | }, [&] (auto &listener) { 274 | THEN("the task must not have been executed") { 275 | REQUIRE(execution_count == 0); 276 | } 277 | 278 | AND_WHEN("the event loop is stimulated with the current time value") { 279 | loop.process(clock); 280 | 281 | THEN("the task must have been executed once") { 282 | REQUIRE(execution_count == 1); 283 | 284 | AND_WHEN("the event loop is stimulated with the same time value once more") { 285 | loop.process(clock); 286 | 287 | THEN("the task must have been executed twice") { 288 | REQUIRE(execution_count == 2); 289 | } 290 | 291 | AND_WHEN("the event loop is stimulated with a very different time value") { 292 | clock += 100; 293 | loop.process(clock); 294 | 295 | THEN("the task must have been executed three times") { 296 | REQUIRE(execution_count == 3); 297 | 298 | AND_THEN("the event must still exist") { 299 | REQUIRE_FALSE(listener.expired()); 300 | } 301 | } 302 | } 303 | } 304 | } 305 | } 306 | }); 307 | } 308 | 309 | WHEN("its .wait() function is called with some time delay") { 310 | auto wait_result = attempt([&] { 311 | return loop.wait(100); 312 | }); 313 | 314 | THEN("no exception must have been thrown") { 315 | REQUIRE_FALSE(wait_result.has_error()); 316 | } 317 | 318 | THEN("a pending juro::promise_ptr must have been returned") { 319 | REQUIRE(wait_result.holds_value>()); 320 | 321 | auto promise = wait_result.get_value(); 322 | REQUIRE(static_cast(promise)); 323 | REQUIRE(promise->is_pending()); 324 | 325 | AND_WHEN("a resolve handler is attached to the promise and the delay is elapsed") { 326 | bool promise_resolved = false; 327 | promise->then([&] (auto) { 328 | promise_resolved = true; 329 | }); 330 | 331 | loop.process(110); 332 | 333 | THEN("the promise must have been resolved") { 334 | REQUIRE(promise_resolved); 335 | } 336 | } 337 | } 338 | } 339 | 340 | WHEN("its .timeout() function is called with a promise as parameter") { 341 | auto promise = juro::make_pending(); 342 | auto timeout_result = attempt([&] { 343 | return loop.timeout(100, promise); 344 | }); 345 | 346 | THEN("no exception must have been thrown") { 347 | REQUIRE_FALSE(timeout_result.has_error()); 348 | } 349 | 350 | THEN("it must have returned a promise pointer to a timeout variant type") { 351 | REQUIRE(timeout_result.holds_value< 352 | juro::promise_ptr> 353 | >()); 354 | 355 | auto &timeout_promise = timeout_result.get_value(); 356 | 357 | AND_WHEN("settle handlers are attached to the timeout promise") { 358 | std::variant resolved_value; 359 | std::exception_ptr rejected_value; 360 | 361 | timeout_promise->then( 362 | [&] (auto &result) { 363 | resolved_value = result; 364 | }, 365 | [&] (auto &error) { 366 | rejected_value = error; 367 | } 368 | ); 369 | 370 | AND_WHEN("the initial promise is resolved") { 371 | promise->resolve("resolved"s); 372 | 373 | THEN("the timeout promise must also have been resolved with the same value") { 374 | REQUIRE(timeout_promise->is_resolved()); 375 | REQUIRE(std::holds_alternative(resolved_value)); 376 | REQUIRE(std::get(resolved_value) == "resolved"s); 377 | } 378 | 379 | } 380 | 381 | AND_WHEN("the initial promise is rejected") { 382 | promise->reject("rejected"s); 383 | 384 | THEN("the timeout promise must also have been rejected with the same value") { 385 | REQUIRE(timeout_promise->is_rejected()); 386 | REQUIRE(rescue(rejected_value).holds_error()); 387 | REQUIRE(rescue(rejected_value).get_error() == "rejected"s); 388 | } 389 | } 390 | 391 | AND_WHEN("the timeout is reached") { 392 | loop.process(100); 393 | 394 | THEN("the timeout promise must have been resolved with a fugax::timeout value") { 395 | REQUIRE(timeout_promise->is_resolved()); 396 | REQUIRE(std::holds_alternative(resolved_value)); 397 | } 398 | } 399 | } 400 | } 401 | } 402 | 403 | WHEN("its .timeout() function is called with a promise launcher as parameter") { 404 | juro::promise_ptr promise; 405 | auto timeout_result = attempt([&] { 406 | return loop.timeout(100, [&] (auto &new_promise) { 407 | promise = new_promise; 408 | }); 409 | }); 410 | 411 | THEN("no exception must have been thrown") { 412 | REQUIRE_FALSE(timeout_result.has_error()); 413 | } 414 | 415 | THEN("it must have returned a promise pointer to a timeout variant type") { 416 | REQUIRE(timeout_result.holds_value< 417 | juro::promise_ptr> 418 | >()); 419 | 420 | auto &timeout_promise = timeout_result.get_value(); 421 | 422 | AND_WHEN("settle handlers are attached to the timeout promise") { 423 | std::variant resolved_value; 424 | std::exception_ptr rejected_value; 425 | 426 | timeout_promise->then( 427 | [&] (auto &result) { 428 | resolved_value = result; 429 | }, 430 | [&] (auto &error) { 431 | rejected_value = error; 432 | } 433 | ); 434 | 435 | AND_WHEN("the initial promise is resolved") { 436 | promise->resolve("resolved"s); 437 | 438 | THEN("the timeout promise must also have been resolved with the same value") { 439 | REQUIRE(timeout_promise->is_resolved()); 440 | REQUIRE(std::holds_alternative(resolved_value)); 441 | REQUIRE(std::get(resolved_value) == "resolved"s); 442 | } 443 | 444 | } 445 | 446 | AND_WHEN("the initial promise is rejected") { 447 | promise->reject("rejected"s); 448 | 449 | THEN("the timeout promise must also have been rejected with the same value") { 450 | REQUIRE(timeout_promise->is_rejected()); 451 | REQUIRE(rescue(rejected_value).holds_error()); 452 | REQUIRE(rescue(rejected_value).get_error() == "rejected"s); 453 | } 454 | } 455 | 456 | AND_WHEN("the timeout is reached") { 457 | loop.process(100); 458 | 459 | THEN("the timeout promise must have been resolved with a fugax::timeout value") { 460 | REQUIRE(timeout_promise->is_resolved()); 461 | REQUIRE(std::holds_alternative(resolved_value)); 462 | } 463 | } 464 | } 465 | } 466 | 467 | } 468 | } 469 | } 470 | 471 | SCENARIO("an event guard can be default-constructed", "[fugax]") { 472 | GIVEN("the default event guard constructor") { 473 | WHEN("it is invoked") { 474 | auto result = attempt([] { 475 | [[maybe_unused]] fugax::event_guard guard { }; 476 | }); 477 | 478 | THEN("no exceptions must have been raised") { 479 | REQUIRE_FALSE(result.has_error()); 480 | } 481 | } 482 | } 483 | } 484 | 485 | SCENARIO("an event guard can manage the lifetime of a scheduled event", "[fugax]") { 486 | GIVEN("an event loop") { 487 | fugax::event_loop loop; 488 | 489 | AND_GIVEN("a listener from a scheduled event") { 490 | bool task_executed = false; 491 | auto listener = loop.schedule(100, [&] { 492 | task_executed = true; 493 | }); 494 | 495 | WHEN("an event guard is constructed from the listener") { 496 | auto guard_result = attempt([&] { 497 | [[maybe_unused]] fugax::event_guard guard { listener }; 498 | }); 499 | 500 | THEN("no exception must have been thrown") { 501 | REQUIRE_FALSE(guard_result.has_error()); 502 | } 503 | } 504 | 505 | AND_GIVEN("an event guard managing the event") { 506 | auto *guard = new fugax::event_guard { listener }; 507 | 508 | THEN("the event must still be scheduled") { 509 | REQUIRE_FALSE(task_executed); 510 | REQUIRE_FALSE(listener.expired()); 511 | REQUIRE_FALSE(listener.lock()->is_cancelled()); 512 | } 513 | 514 | WHEN("the guard is destroyed") { 515 | auto delete_result = attempt([&] { 516 | delete guard; 517 | }); 518 | 519 | THEN("no exception must have been thrown") { 520 | REQUIRE_FALSE(delete_result.has_error()); 521 | } 522 | 523 | THEN("the event must still exist") { 524 | REQUIRE_FALSE(listener.expired()); 525 | } 526 | 527 | THEN("the event must have been cancelled") { 528 | REQUIRE(listener.lock()->is_cancelled()); 529 | } 530 | 531 | AND_WHEN("the scheduled time arrives") { 532 | loop.process(110); 533 | 534 | THEN("the task must not have been executed") { 535 | REQUIRE_FALSE(task_executed); 536 | } 537 | 538 | THEN("the event must have been destroyed") { 539 | REQUIRE(listener.expired()); 540 | } 541 | } 542 | } 543 | } 544 | } 545 | 546 | AND_GIVEN("two event guards managing two scheduled events") { 547 | bool task_1_executed = false, task_2_executed = false; 548 | fugax::event_guard guard_1 = 549 | loop.schedule(100, [&] { 550 | task_1_executed = true; 551 | }); 552 | fugax::event_guard guard_2 = 553 | loop.schedule(100, [&] { 554 | task_2_executed = true; 555 | }); 556 | 557 | THEN("both events must be still scheduled") { 558 | REQUIRE_FALSE(guard_1.get().expired()); 559 | REQUIRE_FALSE(guard_2.get().expired()); 560 | } 561 | 562 | WHEN("guard_2 is move-assigned to guard_1") { 563 | auto assignment_result = attempt([&] { 564 | guard_1 = std::move(guard_2); 565 | }); 566 | 567 | THEN("no exception must have been thrown") { 568 | REQUIRE_FALSE(assignment_result.has_error()); 569 | } 570 | 571 | THEN("guard_2 must be empty") { 572 | REQUIRE(guard_2.get().expired()); 573 | } 574 | 575 | THEN("guard_1 must be still scheduled") { 576 | REQUIRE_FALSE(guard_1.get().expired()); 577 | } 578 | 579 | AND_WHEN("the due time of both events arrive") { 580 | loop.process(100); 581 | 582 | THEN("only task_2 must have been executed") { 583 | REQUIRE_FALSE(task_1_executed); 584 | REQUIRE(task_2_executed); 585 | } 586 | } 587 | } 588 | } 589 | } 590 | } 591 | 592 | SCENARIO("an event loop can debounce calls to a functor", "[fugax]") { 593 | GIVEN("an event loop") { 594 | fugax::event_loop loop; 595 | 596 | WHEN("its .debounce() function is called with a functor and some delay as parameters") { 597 | int counter = 0; 598 | auto functor = [&] { counter++; }; 599 | 600 | auto debounce_result = attempt([&] { 601 | return loop.debounce(100, functor); 602 | }); 603 | 604 | THEN("no exception must have been thrown") { 605 | REQUIRE_FALSE(debounce_result.has_error()); 606 | } 607 | 608 | THEN("it must return a debounced functor with the same signature") { 609 | using result_type = decltype(debounce_result); 610 | STATIC_REQUIRE(std::is_invocable_r_v); 611 | 612 | auto &debounced = debounce_result.get_value(); 613 | 614 | AND_WHEN("the debounced functor is called") { 615 | debounced(); 616 | 617 | THEN("the functor must not have been called") { 618 | REQUIRE(counter == 0); 619 | } 620 | 621 | AND_WHEN("more time than the debounce delay has passed") { 622 | test_clock clock; 623 | loop.process(clock.advance(101)); 624 | 625 | THEN("the functor must have been called") { 626 | REQUIRE(counter == 1); 627 | } 628 | } 629 | } 630 | 631 | AND_WHEN( 632 | "the debounced functor is called multiple times, "\ 633 | "over a time span smaller than the debounce delay" 634 | ) { 635 | test_clock clock; 636 | 637 | for(int i = 0; i < 9; i++) { 638 | debounced(); 639 | loop.process(clock.advance(10)); 640 | } 641 | 642 | THEN("the functor must not have been called") { 643 | REQUIRE(counter == 0); 644 | } 645 | } 646 | 647 | AND_WHEN( 648 | "the debounced functor is called multiple times, "\ 649 | "with an interval smaller than the debounce delay" 650 | ) { 651 | test_clock clock; 652 | 653 | for(int i = 0; i < 9; i++) { 654 | debounced(); 655 | loop.process(clock.advance(99)); 656 | } 657 | 658 | THEN("the functor must not have been called") { 659 | REQUIRE(counter == 0); 660 | } 661 | } 662 | 663 | AND_WHEN( 664 | "the debounced functor is called multiple times, "\ 665 | "with an interval greater than the debounce delay" 666 | ) { 667 | test_clock clock; 668 | 669 | for(int i = 0; i < 9; i++) { 670 | debounced(); 671 | loop.process(clock.advance(101)); 672 | } 673 | 674 | THEN("the functor must have been executed that many times also") { 675 | REQUIRE(counter == 9); 676 | } 677 | } 678 | } 679 | } 680 | 681 | AND_GIVEN("a debounced functor that takes a string parameter") { 682 | std::string debounced_string; 683 | auto debounced = loop.debounce(100, [&] (auto &value) { 684 | debounced_string = value; 685 | }); 686 | 687 | WHEN( 688 | "the debounced functor is called multiple times with different values, " \ 689 | "with intervals smaller than the debounce delay" 690 | ) { 691 | test_clock clock; 692 | 693 | debounced("one"s); 694 | loop.process(clock.advance(50)); 695 | 696 | debounced("two"s); 697 | loop.process(clock.advance(50)); 698 | 699 | debounced("three"s); 700 | 701 | AND_WHEN("the debounce delay is allowed to pass") { 702 | loop.process(clock.advance(100)); 703 | 704 | THEN("the initial functor must have been invoked with the last parameter passed") { 705 | REQUIRE(debounced_string == "three"s); 706 | } 707 | } 708 | } 709 | } 710 | } 711 | } 712 | 713 | SCENARIO("an event loop can throttle calls to a functor", "[fugax]") { 714 | GIVEN("an event loop") { 715 | fugax::event_loop loop; 716 | 717 | WHEN("its .throttle() function is called with a functor and some delay as parameters") { 718 | int counter = 0; 719 | auto functor = [&] { counter++; }; 720 | 721 | auto throttle_result = attempt([&] { 722 | return loop.throttle(100, functor); 723 | }); 724 | 725 | THEN("no exception must have been thrown") { 726 | REQUIRE_FALSE(throttle_result.has_error()); 727 | } 728 | 729 | THEN("it must return a functor with the same signature") { 730 | using result_type = decltype(throttle_result); 731 | STATIC_REQUIRE(std::is_invocable_r_v); 732 | 733 | auto &throttled = throttle_result.get_value(); 734 | 735 | AND_WHEN("the throttled functor is invoked") { 736 | throttled(); 737 | 738 | THEN("the functor must have been executed") { 739 | REQUIRE(counter == 1); 740 | } 741 | 742 | AND_WHEN("the throttled functor is invoked again") { 743 | throttled(); 744 | 745 | THEN("the functor must not have been executed this time") { 746 | REQUIRE(counter == 1); 747 | } 748 | } 749 | 750 | AND_WHEN( 751 | "the throttled functor is invoked multiple times, "\ 752 | "over a time span smalled than the throttle delay" 753 | ) { 754 | test_clock clock; 755 | 756 | for(int i = 0; i < 9; i++) { 757 | throttled(); 758 | loop.process(clock.advance(10)); 759 | } 760 | 761 | THEN("the functor must not have been executed any more times") { 762 | REQUIRE(counter == 1); 763 | } 764 | } 765 | 766 | AND_WHEN( 767 | "the throttled functor is invoked multiple times, "\ 768 | "with an interval smaller than the throttle delay" 769 | ) { 770 | test_clock clock; 771 | 772 | fugax::time_type last = 0; 773 | int expected = 1; 774 | 775 | for(int i = 0; i < 9; i++) { 776 | throttled(); 777 | loop.process(clock.advance(99)); 778 | 779 | if(clock - last > 100) { 780 | expected++; 781 | last = clock; 782 | } 783 | } 784 | 785 | THEN( 786 | "the functor must have been executed only so often as the "\ 787 | "delay had passed repeatedly" 788 | ) { 789 | REQUIRE(counter == expected); 790 | } 791 | } 792 | 793 | AND_WHEN( 794 | "the throttled functor is invoked multiple times, "\ 795 | "with an interval greater than the throttle delay" 796 | ) { 797 | test_clock clock; 798 | 799 | for(int i = 0; i < 9; i++) { 800 | throttled(); 801 | loop.process(clock.advance(101)); 802 | } 803 | 804 | THEN("the functor must have been executed that many times") { 805 | REQUIRE(counter == 9); 806 | } 807 | } 808 | } 809 | } 810 | } 811 | } 812 | } --------------------------------------------------------------------------------