|
| 1 | +#ifndef DMQ_DEADLINE_SUBSCRIPTION_H |
| 2 | +#define DMQ_DEADLINE_SUBSCRIPTION_H |
| 3 | + |
| 4 | +/// @file DeadlineSubscription.h |
| 5 | +/// @see https://github.com/DelegateMQ/DelegateMQ |
| 6 | +/// David Lafreniere, 2025. |
| 7 | +/// |
| 8 | +/// @brief RAII helper that combines a DataBus subscription with a deadline timer. |
| 9 | +/// |
| 10 | +/// @details |
| 11 | +/// `DeadlineSubscription<T>` monitors a DataBus topic and fires a user callback |
| 12 | +/// if no message arrives within a configurable deadline window. It is built |
| 13 | +/// entirely from existing DelegateMQ primitives — `DataBus::Subscribe`, `Timer`, |
| 14 | +/// and `ScopedConnection` — and adds no library-internal mechanism. |
| 15 | +/// |
| 16 | +/// **How it works:** |
| 17 | +/// A `Timer` is started at construction time. Every incoming message resets the |
| 18 | +/// timer. If the timer fires before the next message arrives, the `onMissed` |
| 19 | +/// callback is invoked. Both the data handler and the deadline callback are |
| 20 | +/// dispatched to the same optional worker thread. |
| 21 | +/// |
| 22 | +/// **Lifetime:** |
| 23 | +/// The object is non-copyable and non-movable. All resources — the DataBus |
| 24 | +/// connection, the timer expiry connection, and the timer itself — are released |
| 25 | +/// automatically when the object is destroyed. Destruction is always clean: |
| 26 | +/// members are destroyed in reverse declaration order, so the DataBus connection |
| 27 | +/// disconnects before the timer is torn down. |
| 28 | +/// |
| 29 | +/// **`Timer::ProcessTimers()` requirement:** |
| 30 | +/// The deadline timer fires only when `Timer::ProcessTimers()` is called. On |
| 31 | +/// platforms with a running `Thread`, this is typically driven by the thread's |
| 32 | +/// internal timer. On bare-metal targets, call `ProcessTimers()` from the main |
| 33 | +/// super-loop or a SysTick handler. If `ProcessTimers()` is not called, the |
| 34 | +/// deadline callback silently never fires. |
| 35 | +/// |
| 36 | +/// **`onMissed` callback context:** |
| 37 | +/// - With a `thread` argument: the callback is dispatched asynchronously to |
| 38 | +/// that thread, matching the delivery context of the data handler. |
| 39 | +/// - Without a `thread` argument: the callback fires synchronously on whatever |
| 40 | +/// thread calls `Timer::ProcessTimers()`. On bare-metal this may be an ISR — |
| 41 | +/// keep the callback short and non-blocking. |
| 42 | +/// |
| 43 | +/// **Usage:** |
| 44 | +/// @code |
| 45 | +/// dmq::DeadlineSubscription<SensorData> m_watch{ |
| 46 | +/// "sensor/temp", |
| 47 | +/// std::chrono::milliseconds(500), |
| 48 | +/// [](const SensorData& d) { /* handle data */ }, |
| 49 | +/// []() { /* deadline missed — sensor silent */ }, |
| 50 | +/// &m_workerThread |
| 51 | +/// }; |
| 52 | +/// @endcode |
| 53 | + |
| 54 | +#include "DelegateMQ.h" |
| 55 | +#include "extras/databus/DataBus.h" |
| 56 | +#include "extras/util/Timer.h" |
| 57 | +#include <functional> |
| 58 | +#include <string> |
| 59 | + |
| 60 | +namespace dmq { |
| 61 | + |
| 62 | +template <typename T> |
| 63 | +class DeadlineSubscription { |
| 64 | +public: |
| 65 | + /// Construct a deadline-monitored DataBus subscription. |
| 66 | + /// |
| 67 | + /// @param topic DataBus topic to subscribe to. |
| 68 | + /// @param deadline Maximum allowed interval between deliveries. Must be > 0. |
| 69 | + /// @param handler Called on each data delivery. |
| 70 | + /// @param onMissed Called when no delivery arrives within the deadline window. |
| 71 | + /// @param thread Optional worker thread for both callbacks. If nullptr, |
| 72 | + /// handler fires on the publisher's thread and onMissed fires |
| 73 | + /// on the Timer::ProcessTimers() thread. |
| 74 | + DeadlineSubscription( |
| 75 | + const std::string& topic, |
| 76 | + dmq::Duration deadline, |
| 77 | + std::function<void(const T&)> handler, |
| 78 | + std::function<void()> onMissed, |
| 79 | + dmq::IThread* thread = nullptr) |
| 80 | + : m_deadline(deadline) |
| 81 | + { |
| 82 | + // Connect onMissed to the timer expiry signal, dispatching to thread if provided |
| 83 | + if (thread) { |
| 84 | + m_timerConn = m_timer.OnExpired.Connect( |
| 85 | + MakeDelegate(std::move(onMissed), *thread)); |
| 86 | + } else { |
| 87 | + m_timerConn = m_timer.OnExpired.Connect( |
| 88 | + MakeDelegate(std::move(onMissed))); |
| 89 | + } |
| 90 | + |
| 91 | + // Arm the timer immediately. It fires if no delivery arrives within deadline. |
| 92 | + m_timer.Start(m_deadline, false); |
| 93 | + |
| 94 | + // Subscribe and reset the timer on every delivery |
| 95 | + m_conn = DataBus::Subscribe<T>(topic, |
| 96 | + [this, h = std::move(handler)](const T& data) { |
| 97 | + m_timer.Start(m_deadline, false); // reset deadline window |
| 98 | + h(data); |
| 99 | + }, thread); |
| 100 | + } |
| 101 | + |
| 102 | + ~DeadlineSubscription() = default; |
| 103 | + |
| 104 | + DeadlineSubscription(const DeadlineSubscription&) = delete; |
| 105 | + DeadlineSubscription& operator=(const DeadlineSubscription&) = delete; |
| 106 | + DeadlineSubscription(DeadlineSubscription&&) = delete; |
| 107 | + DeadlineSubscription& operator=(DeadlineSubscription&&) = delete; |
| 108 | + |
| 109 | +private: |
| 110 | + // Declaration order controls destruction order (reverse). |
| 111 | + // m_conn disconnects first (no more timer resets via the data lambda), |
| 112 | + // then m_timerConn disconnects (onMissed removed from timer signal), |
| 113 | + // then m_timer destructs (removed from global timer list). |
| 114 | + dmq::Duration m_deadline; |
| 115 | + Timer m_timer; |
| 116 | + dmq::ScopedConnection m_timerConn; |
| 117 | + dmq::ScopedConnection m_conn; |
| 118 | +}; |
| 119 | + |
| 120 | +} // namespace dmq |
| 121 | + |
| 122 | +#endif // DMQ_DEADLINE_SUBSCRIPTION_H |
0 commit comments