morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
subscription_registry.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <any>
5#include <atomic>
6#include <cstddef>
7#include <functional>
8#include <memory>
9#include <typeindex>
10#include <utility>
11#include <vector>
12
13#include "../executor.hpp"
14#include "../strand.hpp"
15
16/// @file
17/// @brief A `Bridge`'s instance subscriptions: at most one callback per
18/// (binding, result type), matched at publish time on the binding's
19/// *current* instance, so a re-pointed handler's subscriptions follow
20/// it; entries whose binding is gone are pruned.
21///
22/// Templated on the binding type so this header does not depend on
23/// `bridge.hpp`, which defines `HandlerBinding` inline: the body touches the
24/// binding's `currentId` only inside member functions, instantiated once the
25/// type is complete.
26
27namespace morph::bridge::detail {
28
29/// @brief Tracks, per `Binding`, at most one callback per result type, and
30/// delivers matching results to every subscriber attached to the
31/// instance a result was produced on.
32///
33/// Belongs to its `Bridge`'s owner: every member but `hasSubscribers()` is
34/// called there, so the list needs no lock. `hasSubscribers()` reads an atomic
35/// count and may be asked from any thread — a backend's, deciding whether a
36/// result needs to reach the owner at all.
37///
38/// @tparam Binding The handler-binding type subscriptions are held against.
39/// Must expose `std::atomic<std::uint64_t> currentId` (read
40/// via `.load()`) -- the instance id `publishResult` matches
41/// on. `Bridge` instantiates this with
42/// `morph::bridge::detail::HandlerBinding`.
43template <typename Binding>
44class SubscriptionRegistry {
45public:
46 /// @brief Registers a result-type subscription for @p binding.
47 ///
48 /// The subscription is stored against the *binding*, not against a fixed
49 /// instance id, and is matched at publish time by comparing the binding's
50 /// current instance. Re-pointing a handler therefore moves its
51 /// subscriptions with it, which is what makes "tell me about the account
52 /// I am looking at" keep working when the user switches accounts.
53 ///
54 /// @param binding Handler binding that owns the subscription.
55 /// @param type Result type being subscribed to.
56 /// @param sink Type-erased delivery callback; receives the boxed result.
57 /// @param exec Executor the callback is delivered on.
58 void addSubscription(const std::shared_ptr<Binding>& binding, std::type_index type,
59 std::function<void(const std::any&)> sink, ::morph::exec::IExecutor* exec) {
60 for (auto& entry : _subscriptions) {
61 auto owner = entry.binding.lock();
62 if (owner && owner.get() == binding.get() && entry.type == type) {
63 entry.sink = std::move(sink); // one callback per (handler, result type)
64 entry.exec = exec;
65 return;
66 }
67 }
68 _subscriptions.emplace_back(binding, type, std::move(sink), exec);
69 _count.store(_subscriptions.size(), std::memory_order_relaxed);
70 }
71
72 /// @brief Removes @p binding's subscription for @p type, if any.
73 /// @param binding Handler binding that owns the subscription.
74 /// @param type Result type to stop hearing about.
75 void removeSubscription(const std::shared_ptr<Binding>& binding, std::type_index type) {
76 std::erase_if(_subscriptions, [&](const Entry& entry) {
77 auto owner = entry.binding.lock();
78 return !owner || (owner.get() == binding.get() && entry.type == type);
79 });
80 _count.store(_subscriptions.size(), std::memory_order_relaxed);
81 }
82
83 /// @brief Whether any subscription is currently registered.
84 ///
85 /// A single relaxed atomic load, so the common case -- a process with no
86 /// subscribers at all -- pays nothing per result: without subscribers a
87 /// result is never boxed and never sent to the owner.
88 /// @return `true` if at least one subscription exists.
89 [[nodiscard]] bool hasSubscribers() const noexcept { return _count.load(std::memory_order_relaxed) != 0U; }
90
91 /// @brief Delivers @p value to every subscriber attached to instance @p mid.
92 ///
93 /// Called for every successful action result. Subscribers are matched on
94 /// *the instance the result was produced on*, so a handler hears about
95 /// work another handler -- or, with a shared instance, another screen
96 /// entirely -- did on the model it is attached to.
97 ///
98 /// The producing handler is notified too: suppressing the echo would force
99 /// every subscriber to special-case "was this mine", which is exactly the
100 /// bookkeeping the feature exists to remove.
101 ///
102 /// The matching sinks are copied out before any is invoked: a sink
103 /// delivered through a null executor runs here, and may subscribe or
104 /// unsubscribe, which would otherwise change the list under the loop.
105 /// @param mid Instance the result was produced on.
106 /// @param type Result type produced.
107 /// @param value Boxed result.
108 void publishResult(::morph::exec::detail::ModelId mid, std::type_index type, const std::any& value) {
109 std::erase_if(_subscriptions, [](const Entry& entry) { return entry.binding.expired(); });
110 _count.store(_subscriptions.size(), std::memory_order_relaxed);
111 std::vector<std::pair<std::function<void(const std::any&)>, ::morph::exec::IExecutor*>> targets;
112 for (const auto& entry : _subscriptions) {
113 auto owner = entry.binding.lock();
114 if (owner && entry.type == type && owner->currentId.load() == mid.v && entry.sink) {
115 targets.emplace_back(entry.sink, entry.exec);
116 }
117 }
118 for (auto& [sink, exec] : targets) {
119 if (exec != nullptr) {
120 exec->post([sink, value] { sink(value); });
121 } else {
122 sink(value);
123 }
124 }
125 }
126
127 /// @brief Number of registered subscriptions, dead ones not yet pruned included.
128 /// @return Size of the subscription list.
129 [[nodiscard]] std::size_t size() const { return _subscriptions.size(); }
130
131private:
132 struct Entry {
133 Entry(std::weak_ptr<Binding> bindingIn, std::type_index typeIn, std::function<void(const std::any&)> sinkIn,
134 ::morph::exec::IExecutor* execIn)
135 : binding{std::move(bindingIn)}, type{typeIn}, sink{std::move(sinkIn)}, exec{execIn} {}
136
137 std::weak_ptr<Binding> binding;
138 std::type_index type;
139 std::function<void(const std::any&)> sink;
140 ::morph::exec::IExecutor* exec = nullptr;
141 };
142
143 std::vector<Entry> _subscriptions;
144 std::atomic<std::size_t> _count{0};
145};
146
147} // namespace morph::bridge::detail
morph's strands: one per model instance, from core-cpp's core::async::KeyedStrands,...
virtual void post(std::function< void()> task)=0
Schedules task for asynchronous execution.