23namespace morph::reactive {
54 requires std::default_initializable<T>
55 :
Node{runtime.core()}, _value{} {}
62 :
Node{runtime.
core()}, _value{std::move(initial)}, _policy{policy} {}
76 [[nodiscard]] T
const&
get()
const {
77 if (
core().checkOwner()) {
85 [[nodiscard]] T
const&
peek() const noexcept {
return _value; }
99 _value = std::move(value);
106 template <
typename F>
107 requires std::invocable<F, T&>
112 std::invoke(std::forward<F>(fn), _value);
117 [[nodiscard]]
bool writable() const noexcept {
118 if (!
core().checkOwner()) {
121 if (
core().isComputing()) {
129 detail::BatchScope
const batch{
core()};
134 bool recompute()
override {
return false; }
176 template <
typename F>
177 requires std::invocable<F&> && std::move_constructible<F> && std::convertible_to<std::invoke_result_t<F&>, T>
179 :
Node{runtime.
core()}, _fn{std::make_shared<F>(std::move(fn))}, _compute{&compute<F>}, _policy{policy} {
198 [[nodiscard]] T
const&
get()
const {
199 if (!
core().checkOwner()) {
204 return mutableSelf().current();
212 [[nodiscard]] T
const&
peek()
const {
213 if (!
core().checkOwner()) {
218 return mutableSelf().current();
226 explicit Computing(
detail::RuntimeCore& core) noexcept : _core{&core} { core.enterComputed(); }
228 Computing(Computing
const&) =
delete;
229 Computing& operator=(Computing
const&) =
delete;
230 Computing(Computing&&) =
delete;
231 Computing& operator=(Computing&&) =
delete;
234 detail::RuntimeCore* _core;
237 template <
typename F>
238 static T compute(
void* fn) {
239 return std::invoke(*
static_cast<F*
>(fn));
243 [[nodiscard]]
Computed& mutableSelf() const noexcept {
244 return const_cast<Computed&
>(*this);
247 [[nodiscard]] T
const& cached()
const {
248 if (!_value.has_value()) {
249 throw std::logic_error{
"morph::reactive: a Computed read off the owner before it had a value"};
254 void refuseSelfRead()
const {
257 throw std::logic_error{
"morph::reactive: a Computed read itself"};
264 detail::BatchScope
const batch{
core()};
270 if (_error !=
nullptr ? !_errorUnread : !_value.has_value()) {
275 if (_error !=
nullptr) {
276 _errorUnread =
false;
277 std::rethrow_exception(_error);
279 if (!_value.has_value()) {
280 throw std::logic_error{
"morph::reactive: a Computed has no value"};
287 throw std::logic_error{
"morph::reactive: a Computed was destroyed by its own computation"};
291 bool recompute()
override {
294 std::shared_ptr<void>
const fn = _fn;
295 T (*
const run)(
void*) = _compute;
296 detail::TrackingFrame frame{
core(),
this};
297 bool const isRetry = std::exchange(_isRetry,
false);
298 std::optional<T> fresh;
299 std::exception_ptr error;
301 Computing
const computing{
core()};
303 fresh.emplace(
run(fn.get()));
305 error = std::current_exception();
308 if (frame.observerDestroyed()) {
311 if (error !=
nullptr) {
312 bool const repeated = isRetry && _error !=
nullptr;
319 bool const recovered = std::exchange(_error,
nullptr) !=
nullptr;
320 if constexpr (kEqualityUsable<T>) {
321 if (_policy == EqualityPolicy::Auto && !recovered && _value.has_value() && *_value == *fresh) {
325 _value.emplace(std::move(*fresh));
329 std::shared_ptr<void> _fn;
330 T (*_compute)(
void*);
332 std::optional<T> _value;
334 std::exception_ptr _error;
336 bool _errorUnread =
false;
338 bool _isRetry =
false;
359 template <
typename F>
360 requires std::invocable<F&> && std::move_constructible<F>
362 :
Node{runtime.
core(),
depth}, _body{std::make_shared<F>(std::move(fn))}, _run{&runBody<F>} {
381 [[nodiscard]]
bool isEffect() const noexcept
override {
return true; }
383 template <
typename F>
384 static void runBody(
void* body) {
385 std::invoke(*
static_cast<F*
>(body));
388 bool recompute()
override {
390 std::shared_ptr<void>
const body = _body;
391 void (*
const run)(
void*) = _run;
392 detail::TrackingFrame frame{
core(),
this};
396 if (!frame.observerDestroyed()) {
401 if (!frame.observerDestroyed()) {
407 std::shared_ptr<void> _body;
Command run(MutationHandle< Action > const &mutation, Body< Action > const &body={}, RunOptions const &options={})
run: runs a mutation.
A derived value: computed lazily on first read, cached, and recomputed only after a source it read re...
T const & peek() const
Reads the value, bringing it up to date first, without subscribing anything.
T const & get() const
Reads the value, bringing it up to date first, and subscribes the innermost tracked run.
Computed(Runtime &runtime, F fn, EqualityPolicy policy=EqualityPolicy::Auto)
Constructs the Computed without computing it: the first read does.
A side effect: runs once on construction, then in a flush after any source it read changed.
Effect(Runtime &runtime, F fn, std::size_t depth=0)
Constructs the Effect and runs it once. Off the owner this is reported (detail::site::kOffOwner) and ...
The owner of a signal graph: every node takes one, and every operation runs on its owner.
A source value. Reading it with get() inside a tracked run subscribes that run.
void mutate(F &&fn)
Changes the value in place and always notifies observers.
Signal(Runtime &runtime)
Constructs a value-initialised signal.
T const & get() const
Reads the value, subscribing the innermost tracked run.
Signal(Runtime &runtime, T initial, EqualityPolicy policy=EqualityPolicy::Auto)
Constructs a signal holding initial.
void set(T value)
Replaces the value and notifies observers, unless it equals the current one and the policy is Equalit...
T const & peek() const noexcept
Reads the value without subscribing anything.
A graph node: a source (Signal), an observer (Effect), or both (Computed).
std::size_t depth() const noexcept
How deeply the scope owning this node is nested; the flush runs shallower Effects first.
void trackRead() const
Registers this node as a source of the innermost tracked run, if any.
bool bornOnOwner() const noexcept
Whether this node was constructed on the owner; one that was not is refused its work.
Node(std::shared_ptr< RuntimeCore > core, std::size_t depth=0)
Constructs a node. Off the owner this is reported (site::kOffOwner) and the node is not counted towar...
void forceDirty() noexcept
Marks this node Dirty, so the next pull recomputes it.
bool isPulling() const noexcept
Whether a pull is bringing this node up to date right now. A read of the node from inside that pull c...
void mergeSources(std::vector< Node * > fresh)
Adds fresh to this node's sources without dropping any: what a failed run keeps.
void markObserversStale(Colour colour)
Marks every observer of this node colour.
bool updateIfNecessary()
Pull phase: brings this node up to date, recomputing only if a source really changed.
RuntimeCore & core() const noexcept
The runtime core.
void adoptSources(std::vector< Node * > fresh)
Replaces this node's sources with fresh, unlinking the ones no longer read.
State shared by one Runtime and every node made from it.
void leaveComputed() noexcept
Marks a Computed as done computing.
void report(char const *where) const noexcept
Reports a misuse: asserts in a debug build, or hands it to an installed owner probe.
Collects the sources one tracked run reads, and restores the previous frame when it ends.
The reactive graph's internals: node colours and links, the tracking frame, batching and the flush qu...
constexpr char const * kComputedReadsItself
A Computed read itself; the read throws std::logic_error.
constexpr char const * kEffectThrew
An exception escaped an Effect; the flush stops.
constexpr char const * kSetInComputed
A signal was written while a Computed was computing; the write is dropped.
morph::reactive::Runtime: the owner, batching and flush scheduling of a signal graph.
EqualityPolicy
Whether a Signal or Computed compares a new value with the current one before notifying.
@ Auto
Skip a value equal to the current one, when == is usable for the type (kEqualityUsable).
@ Always
Notify on every write or recomputation, even of an equal value.
constexpr bool kEqualityUsable
Whether == is usable for T, which is what the EqualityPolicy::Auto skip needs.
Whether == is usable for T: declared, and its element types usable too.