Commit 04e02412 authored by Dominik Charousset's avatar Dominik Charousset

Make most operators *cold* by default

parent a52cde49
...@@ -129,6 +129,7 @@ caf_add_component( ...@@ -129,6 +129,7 @@ caf_add_component(
src/detail/print.cpp src/detail/print.cpp
src/detail/private_thread.cpp src/detail/private_thread.cpp
src/detail/private_thread_pool.cpp src/detail/private_thread_pool.cpp
src/detail/ref_counted_base.cpp
src/detail/ripemd_160.cpp src/detail/ripemd_160.cpp
src/detail/serialized_size.cpp src/detail/serialized_size.cpp
src/detail/set_thread_name.cpp src/detail/set_thread_name.cpp
...@@ -147,8 +148,10 @@ caf_add_component( ...@@ -147,8 +148,10 @@ caf_add_component(
src/error.cpp src/error.cpp
src/event_based_actor.cpp src/event_based_actor.cpp
src/execution_unit.cpp src/execution_unit.cpp
src/flow/coordinated.cpp
src/flow/coordinator.cpp src/flow/coordinator.cpp
src/flow/observable_builder.cpp src/flow/observable_builder.cpp
src/flow/op/interval.cpp
src/flow/scoped_coordinator.cpp src/flow/scoped_coordinator.cpp
src/flow/subscription.cpp src/flow/subscription.cpp
src/forwarding_actor_proxy.cpp src/forwarding_actor_proxy.cpp
...@@ -285,17 +288,18 @@ caf_add_component( ...@@ -285,17 +288,18 @@ caf_add_component(
dynamic_spawn dynamic_spawn
error error
expected expected
flow.broadcaster
flow.concat flow.concat
flow.concat_map flow.concat_map
flow.defer flow.defer
flow.empty flow.empty
flow.error flow.fail
flow.flat_map flow.flat_map
flow.for_each flow.for_each
flow.generation flow.generation
flow.interval flow.interval
flow.item_publisher
flow.merge flow.merge
flow.mixed
flow.never flow.never
flow.observe_on flow.observe_on
flow.prefix_and_tail flow.prefix_and_tail
......
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include <atomic>
#include <cstddef>
#include "caf/detail/core_export.hpp"
namespace caf::detail {
// Base class for reference counted objects with an atomic reference count.
class CAF_CORE_EXPORT ref_counted_base {
public:
virtual ~ref_counted_base();
ref_counted_base();
ref_counted_base(const ref_counted_base&);
ref_counted_base& operator=(const ref_counted_base&);
/// Increases reference count by one.
void ref() const noexcept {
rc_.fetch_add(1, std::memory_order_relaxed);
}
/// Decreases reference count by one and calls `request_deletion`
/// when it drops to zero.
void deref() const noexcept;
/// Queries whether there is exactly one reference.
bool unique() const noexcept {
return rc_ == 1;
}
size_t get_reference_count() const noexcept {
return rc_.load();
}
protected:
mutable std::atomic<size_t> rc_;
};
} // namespace caf::detail
...@@ -222,6 +222,9 @@ public: ...@@ -222,6 +222,9 @@ public:
template <class T> template <class T>
constexpr bool is_iterable<T>::value; constexpr bool is_iterable<T>::value;
template <class T>
constexpr bool is_iterable_v = is_iterable<T>::value;
/// Checks whether `T` is a non-const reference. /// Checks whether `T` is a non-const reference.
template <class T> template <class T>
struct is_mutable_ref : std::false_type { }; struct is_mutable_ref : std::false_type { };
......
...@@ -35,13 +35,13 @@ public: ...@@ -35,13 +35,13 @@ public:
virtual void deref_disposable() const noexcept = 0; virtual void deref_disposable() const noexcept = 0;
}; };
// -- constructors, destructors, and assignment operators --------------------
explicit disposable(intrusive_ptr<impl> pimpl) noexcept explicit disposable(intrusive_ptr<impl> pimpl) noexcept
: pimpl_(std::move(pimpl)) { : pimpl_(std::move(pimpl)) {
// nop // nop
} }
// -- constructors, destructors, and assignment operators --------------------
disposable() noexcept = default; disposable() noexcept = default;
disposable(disposable&&) noexcept = default; disposable(disposable&&) noexcept = default;
...@@ -57,6 +57,7 @@ public: ...@@ -57,6 +57,7 @@ public:
return *this; return *this;
} }
// -- factories -------------------------------------------------------------- // -- factories --------------------------------------------------------------
/// Combines multiple disposables into a single disposable. The new disposable /// Combines multiple disposables into a single disposable. The new disposable
......
...@@ -7,21 +7,4 @@ ...@@ -7,21 +7,4 @@
#include "caf/flow/observable.hpp" #include "caf/flow/observable.hpp"
#include "caf/flow/observer.hpp" #include "caf/flow/observer.hpp"
namespace caf::flow { namespace caf::flow {} // namespace caf::flow
/// Combines the items emitted from `pub` and `pubs...` to appear as a single
/// stream of items.
template <class Observable, class... Observables>
observable<typename Observable::output_type>
concat(Observable x, Observables... xs) {
using output_type = output_type_t<Observable>;
static_assert(
(std::is_same_v<output_type, output_type_t<Observables>> && ...));
auto hdl = std::move(x).as_observable();
auto ptr = make_counted<concat_impl<output_type>>(hdl.ptr()->ctx());
ptr->add(std::move(hdl));
(ptr->add(std::move(xs).as_observable()), ...);
return observable<output_type>{std::move(ptr)};
}
} // namespace caf::flow
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/detail/core_export.hpp"
namespace caf::flow {
/// An object that lives on a @ref coordinator.
class CAF_CORE_EXPORT coordinated {
public:
// -- constructors, destructors, and assignment operators --------------------
virtual ~coordinated();
// -- reference counting -----------------------------------------------------
/// Increases the reference count of the coordinated.
virtual void ref_coordinated() const noexcept = 0;
/// Decreases the reference count of the coordinated and destroys the object
/// if necessary.
virtual void deref_coordinated() const noexcept = 0;
friend void intrusive_ptr_add_ref(const coordinated* ptr) noexcept {
ptr->ref_coordinated();
}
friend void intrusive_ptr_release(const coordinated* ptr) noexcept {
ptr->deref_coordinated();
}
};
} // namespace caf::flow
...@@ -9,7 +9,6 @@ ...@@ -9,7 +9,6 @@
#include "caf/flow/fwd.hpp" #include "caf/flow/fwd.hpp"
#include "caf/flow/subscription.hpp" #include "caf/flow/subscription.hpp"
#include "caf/intrusive_ptr.hpp" #include "caf/intrusive_ptr.hpp"
#include "caf/ref_counted.hpp"
#include "caf/timespan.hpp" #include "caf/timespan.hpp"
#include <chrono> #include <chrono>
...@@ -22,10 +21,6 @@ namespace caf::flow { ...@@ -22,10 +21,6 @@ namespace caf::flow {
/// objects since the coordinator guarantees synchronous execution. /// objects since the coordinator guarantees synchronous execution.
class CAF_CORE_EXPORT coordinator { class CAF_CORE_EXPORT coordinator {
public: public:
// -- friends ----------------------------------------------------------------
friend class subscription_impl;
// -- member types ----------------------------------------------------------- // -- member types -----------------------------------------------------------
/// A time point of the monotonic clock. /// A time point of the monotonic clock.
......
...@@ -121,9 +121,6 @@ struct output_type_oracle { ...@@ -121,9 +121,6 @@ struct output_type_oracle {
template <class T> template <class T>
using output_type_t = typename output_type_oracle<T>::type; using output_type_t = typename output_type_oracle<T>::type;
template <class T>
class merger_impl;
template <class> template <class>
struct has_impl_include { struct has_impl_include {
static constexpr bool value = false; static constexpr bool value = false;
......
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/flow/coordinator.hpp"
#include "caf/flow/observable.hpp"
#include "caf/flow/op/mcast.hpp"
#include "caf/intrusive_ptr.hpp"
#include "caf/make_counted.hpp"
#include <cstdint>
namespace caf::flow {
template <class T>
class item_publisher {
public:
explicit item_publisher(coordinator* ctx) {
pimpl_ = make_counted<op::mcast<T>>(ctx);
}
item_publisher(item_publisher&&) = default;
item_publisher& operator=(item_publisher&&) = default;
~item_publisher() {
pimpl_->close();
}
/// Pushes an item to all subscribed observers. The publisher drops the item
/// if no subscriber exists.
void push(const T& item) {
pimpl_->push_all(item);
}
/// Pushes the items in range `[first, last)` to all subscribed observers. The
/// publisher drops the items if no subscriber exists.
template <class Iterator, class Sentinel>
void push(Iterator first, Sentinel last) {
while (first != last)
push(*first++);
}
/// Pushes the items from the initializer list to all subscribed observers.
/// The publisher drops the items if no subscriber exists.
void push(std::initializer_list<T> items) {
for (auto& item : items)
push(item);
}
/// Closes the publisher, eventually emitting on_complete on all observers.
void close() {
pimpl_->close();
}
/// Closes the publisher, eventually emitting on_error on all observers.
void abort(const error& reason) {
pimpl_->abort(reason);
}
/// Queries how many items the publisher may emit immediately to subscribed
/// observers.
size_t demand() const noexcept {
return pimpl_->min_demand();
}
/// Queries how many items are currently waiting in a buffer until the
/// observer requests additional items.
size_t buffered() const noexcept {
return pimpl_->max_buffered();
}
/// Queries whether there is at least one observer subscribed to the operator.
bool has_observers() const noexcept {
return pimpl_->has_observers();
}
/// Converts the publisher to an @ref observable.
observable<T> as_observable() const {
return observable<T>{pimpl_};
}
/// Subscribes a new @ref observer to the output of the publisher.
disposable subscribe(observer<T> out) {
return pimpl_->subscribe(out);
}
private:
intrusive_ptr<op::mcast<T>> pimpl_;
};
} // namespace caf::flow
...@@ -11,17 +11,5 @@ namespace caf::flow { ...@@ -11,17 +11,5 @@ namespace caf::flow {
/// Combines the items emitted from `pub` and `pubs...` to appear as a single /// Combines the items emitted from `pub` and `pubs...` to appear as a single
/// stream of items. /// stream of items.
template <class Observable, class... Observables>
observable<typename Observable::output_type>
merge(Observable x, Observables... xs) {
using output_type = output_type_t<Observable>;
static_assert(
(std::is_same_v<output_type, output_type_t<Observables>> && ...));
auto hdl = std::move(x).as_observable();
auto ptr = make_counted<merger_impl<output_type>>(hdl.ptr()->ctx());
ptr->add(std::move(hdl));
(ptr->add(std::move(xs).as_observable()), ...);
return observable<output_type>{std::move(ptr)};
}
} // namespace caf::flow } // namespace caf::flow
...@@ -4,11 +4,6 @@ ...@@ -4,11 +4,6 @@
#pragma once #pragma once
#include <cstddef>
#include <numeric>
#include <type_traits>
#include <vector>
#include "caf/async/consumer.hpp" #include "caf/async/consumer.hpp"
#include "caf/async/producer.hpp" #include "caf/async/producer.hpp"
#include "caf/async/spsc_buffer.hpp" #include "caf/async/spsc_buffer.hpp"
...@@ -17,10 +12,18 @@ ...@@ -17,10 +12,18 @@
#include "caf/detail/core_export.hpp" #include "caf/detail/core_export.hpp"
#include "caf/detail/unordered_flat_map.hpp" #include "caf/detail/unordered_flat_map.hpp"
#include "caf/disposable.hpp" #include "caf/disposable.hpp"
#include "caf/flow/coordinated.hpp"
#include "caf/flow/coordinator.hpp" #include "caf/flow/coordinator.hpp"
#include "caf/flow/fwd.hpp" #include "caf/flow/fwd.hpp"
#include "caf/flow/observable_decl.hpp"
#include "caf/flow/observable_state.hpp" #include "caf/flow/observable_state.hpp"
#include "caf/flow/observer.hpp" #include "caf/flow/observer.hpp"
#include "caf/flow/op/base.hpp"
#include "caf/flow/op/concat.hpp"
#include "caf/flow/op/from_resource.hpp"
#include "caf/flow/op/from_steps.hpp"
#include "caf/flow/op/merge.hpp"
#include "caf/flow/op/prefix_and_tail.hpp"
#include "caf/flow/step.hpp" #include "caf/flow/step.hpp"
#include "caf/flow/subscription.hpp" #include "caf/flow/subscription.hpp"
#include "caf/intrusive_ptr.hpp" #include "caf/intrusive_ptr.hpp"
...@@ -29,330 +32,13 @@ ...@@ -29,330 +32,13 @@
#include "caf/ref_counted.hpp" #include "caf/ref_counted.hpp"
#include "caf/sec.hpp" #include "caf/sec.hpp"
namespace caf::flow { #include <cstddef>
#include <functional>
/// Represents a potentially unbound sequence of values. #include <numeric>
template <class T> #include <type_traits>
class observable { #include <vector>
public:
using output_type = T;
/// Internal interface of an `observable`.
class impl : public disposable_impl {
public:
// -- member types ---------------------------------------------------------
using output_type = T;
// -- properties -----------------------------------------------------------
virtual coordinator* ctx() const noexcept = 0;
// -- conversions ----------------------------------------------------------
observable as_observable() noexcept;
// -- flow processing ------------------------------------------------------
/// Subscribes a new observer.
virtual disposable subscribe(observer<T> what) = 0;
// -- convenience functions ------------------------------------------------
/// Calls `on_error` on the `sink` with given `code` and returns a
/// default-constructed @ref disposable.
disposable reject_subscription(observer_impl<T>* sink, sec code) {
sink->on_error(make_error(code));
return disposable{};
}
/// @copydoc reject_subscription
disposable reject_subscription(observer<T>& sink, sec code) {
return reject_subscription(sink.ptr(), code);
}
};
explicit observable(intrusive_ptr<impl> pimpl) noexcept
: pimpl_(std::move(pimpl)) {
// nop
}
observable& operator=(std::nullptr_t) noexcept {
pimpl_.reset();
return *this;
}
observable() noexcept = default;
observable(observable&&) noexcept = default;
observable(const observable&) noexcept = default;
observable& operator=(observable&&) noexcept = default;
observable& operator=(const observable&) noexcept = default;
void dispose() {
if (pimpl_) {
pimpl_->dispose();
pimpl_ = nullptr;
}
}
disposable as_disposable() const& noexcept {
return disposable{pimpl_};
}
disposable as_disposable() && noexcept {
return disposable{std::move(pimpl_)};
}
/// @copydoc impl::subscribe
disposable subscribe(observer<T> what) {
if (pimpl_) {
return pimpl_->subscribe(std::move(what));
} else {
what.on_error(make_error(sec::invalid_observable));
return disposable{};
}
}
/// Creates a new observer that pushes all observed items to the resource.
disposable subscribe(async::producer_resource<T> resource);
/// Returns a transformation that applies a step function to each input.
template <class Step>
transformation<Step> transform(Step step);
/// Registers a callback for `on_complete` events.
template <class F>
auto do_on_complete(F f) {
return transform(do_on_complete_step<T, F>{std::move(f)});
}
/// Registers a callback for `on_error` events.
template <class F>
auto do_on_error(F f) {
return transform(do_on_error_step<T, F>{std::move(f)});
}
/// Registers a callback that runs on `on_complete` or `on_error`.
template <class F>
auto do_finally(F f) {
return transform(do_finally_step<T, F>{std::move(f)});
}
/// Catches errors by converting them into errors instead.
auto on_error_complete() {
return transform(on_error_complete_step<T>{});
}
/// Returns a transformation that selects only the first `n` items.
transformation<limit_step<T>> take(size_t n);
/// Returns a transformation that selects only items that satisfy `predicate`.
template <class Predicate>
transformation<filter_step<Predicate>> filter(Predicate prediate);
/// Returns a transformation that applies `f` to each input and emits the
/// result of the function application.
template <class F>
transformation<map_step<F>> map(F f);
/// Calls `on_next` for each item emitted by this observable.
template <class OnNext>
disposable for_each(OnNext on_next);
/// Calls `on_next` for each item and `on_error` for each error emitted by
/// this observable.
template <class OnNext, class OnError>
disposable for_each(OnNext on_next, OnError on_error);
/// Calls `on_next` for each item, `on_error` for each error and `on_complete`
/// for each completion signal emitted by this observable.
template <class OnNext, class OnError, class OnComplete>
disposable for_each(OnNext on_next, OnError on_error, OnComplete on_complete);
/// Returns a transformation that emits items by merging the outputs of all
/// observables returned by `f`.
template <class F>
auto flat_map(F f);
/// Returns a transformation that emits items from optional values returned by
/// `f`.
template <class F>
transformation<flat_map_optional_step<F>> flat_map_optional(F f);
/// Returns a transformation that emits items by concatenating the outputs of
/// all observables returned by `f`.
template <class F>
auto concat_map(F f);
/// Takes @p prefix_size elements from this observable and emits it in a tuple
/// containing an observable for the remaining elements as the second value.
/// The returned observable either emits a single element (the tuple) or none
/// if this observable never produces sufficient elements for the prefix.
/// @pre `prefix_size > 0`
observable<cow_tuple<std::vector<T>, observable<T>>>
prefix_and_tail(size_t prefix_size);
/// Similar to `prefix_and_tail(1)` but passes the single element directly in
/// the tuple instead of wrapping it in a list.
observable<cow_tuple<T, observable<T>>> head_and_tail();
/// Creates an asynchronous resource that makes emitted items available in a
/// spsc buffer.
async::consumer_resource<T> to_resource(size_t buffer_size,
size_t min_request_size);
async::consumer_resource<T> to_resource() {
return to_resource(defaults::flow::buffer_size, defaults::flow::min_demand);
}
observable observe_on(coordinator* other, size_t buffer_size,
size_t min_request_size);
observable observe_on(coordinator* other) {
return observe_on(other, defaults::flow::buffer_size,
defaults::flow::min_demand);
}
const observable& as_observable() const& noexcept {
return std::move(*this);
}
observable&& as_observable() && noexcept {
return std::move(*this);
}
bool valid() const noexcept {
return pimpl_ != nullptr;
}
explicit operator bool() const noexcept {
return valid();
}
bool operator!() const noexcept {
return !valid();
}
impl* ptr() {
return pimpl_.get();
}
const impl* ptr() const {
return pimpl_.get();
}
const intrusive_ptr<impl>& as_intrusive_ptr() const& noexcept {
return pimpl_;
}
intrusive_ptr<impl>&& as_intrusive_ptr() && noexcept {
return std::move(pimpl_);
}
void swap(observable& other) {
pimpl_.swap(other.pimpl_);
}
/// @pre `valid()`
coordinator* ctx() const {
return pimpl_->ctx();
}
private:
intrusive_ptr<impl> pimpl_;
};
template <class T>
observable<T> observable<T>::impl::as_observable() noexcept {
return observable<T>{intrusive_ptr{this}};
}
/// @relates observable
template <class T>
using observable_impl = typename observable<T>::impl;
/// Default base type for observable implementation types.
template <class T>
class observable_impl_base : public ref_counted,
public observable_impl<T>,
public subscription::listener {
public:
// -- member types -----------------------------------------------------------
using super = observable_impl<T>;
using output_type = T;
// -- constructors, destructors, and assignment operators --------------------
explicit observable_impl_base(coordinator* ctx) : ctx_(ctx) {
// nop
}
// -- implementation of disposable_impl --------------------------------------
disposable as_disposable() noexcept {
// We need to implement this only for disambiguation since `as_disposable`
// exists in multiple base classes.
return subscription::listener::as_disposable();
}
void ref_disposable() const noexcept final {
this->ref();
}
void deref_disposable() const noexcept final {
this->deref();
}
// -- implementation of observable_impl<T> -----------------------------------
coordinator* ctx() const noexcept final {
return ctx_;
}
// -- convenience functions --------------------------------------------------
/// Creates a subscription for `sink` and returns a @ref disposable to cancel
/// the observer.
disposable do_subscribe(observer_impl<T>* sink) {
sink->on_subscribe(subscription::default_impl::make(ctx_, this, sink));
// Note: we do NOT return the subscription here because this object is
// private to the observer. Outside code cancels flows by calling
// dispose on the observer.
return sink->as_disposable();
}
/// @copydoc do_subscribe
disposable do_subscribe(observer<T>& sink) {
return do_subscribe(sink.ptr());
}
protected:
// -- member variables -------------------------------------------------------
coordinator* ctx_;
};
/// Convenience function for creating a sub-type of @ref observable_impl without
/// exposing the derived type.
template <class Impl, class... Ts>
intrusive_ptr<observable_impl<typename Impl::output_type>>
make_observable_impl(coordinator* ctx, Ts&&... xs) {
using out_t = typename Impl::output_type;
using res_t = intrusive_ptr<observable_impl<out_t>>;
observable_impl<out_t>* ptr = new Impl(ctx, std::forward<Ts>(xs)...);
return res_t{ptr, false};
}
/// Convenience function for creating an @ref observable from a concrete namespace caf::flow {
/// implementation type.
template <class Impl, class... Ts>
observable<typename Impl::output_type>
make_observable(coordinator* ctx, Ts&&... xs) {
using res_t = observable<typename Impl::output_type>;
return res_t{make_observable_impl<Impl>(ctx, std::forward<Ts>(xs)...)};
}
/// Base type for classes that represent a definition of an `observable` which /// Base type for classes that represent a definition of an `observable` which
/// has not yet been converted to an actual `observable`. /// has not yet been converted to an actual `observable`.
...@@ -363,519 +49,252 @@ public: ...@@ -363,519 +49,252 @@ public:
template <class OnNext> template <class OnNext>
auto for_each(OnNext on_next) && { auto for_each(OnNext on_next) && {
return lift().for_each(std::move(on_next)); return alloc().for_each(std::move(on_next));
} }
template <class OnNext, class OnError> template <class OnNext, class OnError>
auto for_each(OnNext on_next, OnError on_error) && { auto for_each(OnNext on_next, OnError on_error) && {
return lift().for_each(std::move(on_next), std::move(on_error)); return alloc().for_each(std::move(on_next), std::move(on_error));
} }
template <class OnNext, class OnError, class OnComplete> template <class OnNext, class OnError, class OnComplete>
auto for_each(OnNext on_next, OnError on_error, OnComplete on_complete) && { auto for_each(OnNext on_next, OnError on_error, OnComplete on_complete) && {
return lift().for_each(std::move(on_next), std::move(on_error), return alloc().for_each(std::move(on_next), std::move(on_error),
std::move(on_complete)); std::move(on_complete));
} }
template <class... Inputs>
auto merge(Inputs&&... xs) && {
return alloc().as_observable().merge(std::forward<Inputs>(xs)...);
}
template <class... Inputs>
auto concat(Inputs&&... xs) && {
return alloc().as_observable().concat(std::forward<Inputs>(xs)...);
}
template <class F> template <class F>
auto flat_map(F f) && { auto flat_map(F f) && {
return lift().flat_map(std::move(f)); return alloc().flat_map(std::move(f));
} }
template <class F> template <class F>
auto concat_map(F f) && { auto concat_map(F f) && {
return lift().concat_map(std::move(f)); return alloc().concat_map(std::move(f));
} }
observable<cow_tuple<std::vector<T>, observable<T>>> observable<cow_tuple<std::vector<T>, observable<T>>>
prefix_and_tail(size_t prefix_size) && { prefix_and_tail(size_t prefix_size) && {
return lift().prefix_and_tail(prefix_size); return alloc().prefix_and_tail(prefix_size);
} }
observable<cow_tuple<T, observable<T>>> head_and_tail() && { observable<cow_tuple<T, observable<T>>> head_and_tail() && {
return lift().head_and_tail(); return alloc().head_and_tail();
} }
disposable subscribe(observer<T> what) && { disposable subscribe(observer<T> what) && {
return lift().subscribe(std::move(what)); return alloc().subscribe(std::move(what));
} }
disposable subscribe(async::producer_resource<T> resource) && { disposable subscribe(async::producer_resource<T> resource) && {
return lift().subscribe(std::move(resource)); return alloc().subscribe(std::move(resource));
} }
async::consumer_resource<T> to_resource() && { async::consumer_resource<T> to_resource() && {
return lift().to_resource(); return alloc().to_resource();
} }
async::consumer_resource<T> to_resource(size_t buffer_size, async::consumer_resource<T> to_resource(size_t buffer_size,
size_t min_request_size) && { size_t min_request_size) && {
return lift().to_resource(buffer_size, min_request_size); return alloc().to_resource(buffer_size, min_request_size);
} }
observable<T> observe_on(coordinator* other) && { observable<T> observe_on(coordinator* other) && {
return lift().observe_on(other); return alloc().observe_on(other);
} }
observable<T> observe_on(coordinator* other, size_t buffer_size, observable<T> observe_on(coordinator* other, size_t buffer_size,
size_t min_request_size) && { size_t min_request_size) && {
return lift().observe_on(other, buffer_size, min_request_size); return alloc().observe_on(other, buffer_size, min_request_size);
} }
virtual observable<T> as_observable() && = 0; virtual observable<T> as_observable() && = 0;
private: private:
decltype(auto) lift() { /// Allocates and returns an actual @ref observable.
decltype(auto) alloc() {
return std::move(*this).as_observable(); return std::move(*this).as_observable();
} }
}; };
template <class In, class Out> // -- transformation -----------------------------------------------------------
class processor {
/// A special type of observer that applies a series of transformation steps to
/// its input before broadcasting the result as output.
template <class Step, class... Steps>
class transformation final
: public observable_def<steps_output_type_t<Step, Steps...>> {
public: public:
class impl : public observer_impl<In>, public observable_impl<Out> { using input_type = typename Step::input_type;
public:
observer_impl<In>* as_observer_ptr() noexcept {
return this;
}
observable_impl<In>* as_observable_ptr() noexcept { using output_type = steps_output_type_t<Step, Steps...>;
return this;
}
};
explicit processor(intrusive_ptr<impl> pimpl) noexcept template <class Tuple>
: pimpl_(std::move(pimpl)) { transformation(observable<input_type> source, Tuple&& steps)
: source_(std::move(source)), steps_(std::move(steps)) {
// nop // nop
} }
processor& operator=(std::nullptr_t) noexcept { transformation() = delete;
pimpl_.reset(); transformation(const transformation&) = delete;
return *this; transformation& operator=(const transformation&) = delete;
}
using input_type = In;
using output_type = Out;
processor() noexcept = default;
processor(processor&&) noexcept = default;
processor(const processor&) noexcept = default;
processor& operator=(processor&&) noexcept = default;
processor& operator=(const processor&) noexcept = default;
// -- conversion ------------------------------------------------------------- transformation(transformation&&) = default;
transformation& operator=(transformation&&) = default;
disposable as_disposable() const& noexcept { /// @copydoc observable::transform
return as_observable().as_disposable(); template <class NewStep>
transformation<Step, Steps..., NewStep> transform(NewStep step) && {
return {std::move(source_),
std::tuple_cat(std::move(steps_),
std::make_tuple(std::move(step)))};
} }
disposable as_disposable() && noexcept { auto take(size_t n) && {
return std::move(*this).as_observable().as_disposable(); return std::move(*this).transform(limit_step<output_type>{n});
} }
observer<In> as_observer() const& noexcept { template <class Predicate>
return pimpl_->as_observer(); auto filter(Predicate predicate) && {
return std::move(*this).transform(
filter_step<Predicate>{std::move(predicate)});
} }
observer<In> as_observer() && noexcept { template <class Predicate>
auto raw = pimpl_.release()->as_observer_ptr(); auto take_while(Predicate predicate) && {
auto ptr = intrusive_ptr<observer_impl<In>>{raw, false}; return std::move(*this).transform(
return observer<In>{std::move(ptr)}; take_while_step<Predicate>{std::move(predicate)});
} }
observable<Out> as_observable() const& noexcept { template <class Reducer>
return pimpl_->as_observable(); auto reduce(output_type init, Reducer reducer) && {
return std::move(*this).transform(
reduce_step<output_type, Reducer>{init, reducer});
} }
observable<Out> as_observable() && noexcept { auto sum() && {
auto raw = pimpl_.release()->as_observable_ptr(); return std::move(*this).reduce(output_type{}, std::plus<output_type>{});
auto ptr = intrusive_ptr<observable_impl<Out>>{raw, false};
return observable<Out>{std::move(ptr)};
} }
private: auto distinct() && {
intrusive_ptr<impl> pimpl_; return std::move(*this).transform(distinct_step<output_type>{});
};
/// @relates processor
template <class In, class Out = In>
using processor_impl = typename processor<In, Out>::impl;
/// Default base type for processor implementation types.
/// @relates processor
template <class In, class Out>
class processor_impl_base : public ref_counted,
public processor_impl<In, Out>,
public subscription::listener {
public:
// -- member types -----------------------------------------------------------
using super = processor_impl<In, Out>;
using input_type = In;
using output_type = Out;
// -- constructors, destructors, and assignment operators --------------------
explicit processor_impl_base(coordinator* ctx) : ctx_(ctx) {
// nop
} }
// -- implementation of disposable_impl -------------------------------------- template <class F>
auto map(F f) && {
disposable as_disposable() noexcept { return std::move(*this).transform(map_step<F>{std::move(f)});
// We need to implement this only for disambiguation since `as_disposable`
// exists in multiple base classes.
return subscription::listener::as_disposable();
} }
void ref_disposable() const noexcept final { template <class F>
this->ref(); auto flat_map_optional(F f) && {
return std::move(*this).transform(flat_map_optional_step<F>{std::move(f)});
} }
void deref_disposable() const noexcept final { template <class F>
this->deref(); auto do_on_next(F f) && {
return std::move(*this) //
.transform(do_on_next_step<output_type, F>{std::move(f)});
} }
// -- implementation of processor_impl<T> ----------------------------------- template <class F>
auto do_on_complete(F f) && {
coordinator* ctx() const noexcept final { return std::move(*this) //
return ctx_; .transform(do_on_complete_step<output_type, F>{std::move(f)});
} }
// -- convenience functions -------------------------------------------------- template <class F>
auto do_on_error(F f) && {
return std::move(*this) //
.transform(do_on_error_step<output_type, F>{std::move(f)});
}
/// Creates a subscription for `sink` and returns a @ref disposable to cancel template <class F>
/// the observer. auto do_finally(F f) && {
disposable do_subscribe(observer_impl<Out>* sink) { return std::move(*this) //
sink->on_subscribe(subscription::default_impl::make(ctx_, this, sink)); .transform(do_finally_step<output_type, F>{std::move(f)});
// Note: we do NOT return the subscription here because this object is
// private to the observer. Outside code cancels flows by calling
// dispose on the observer.
return disposable{intrusive_ptr<disposable_impl>{sink}};
} }
/// @copydoc do_subscribe auto on_error_complete() {
disposable do_subscribe(observer<Out>& sink) { return std::move(*this) //
return do_subscribe(sink.ptr()); .transform(on_error_complete_step<output_type>{});
} }
protected: observable<output_type> as_observable() && {
// -- member variables ------------------------------------------------------- using impl_t = op::from_steps<input_type, Step, Steps...>;
return make_observable<impl_t>(source_.ctx(), source_.pimpl(),
std::move(steps_));
}
coordinator* ctx_; private:
observable<input_type> source_;
std::tuple<Step, Steps...> steps_;
}; };
// -- representing an error as an observable ----------------------------------- // -- observable::transform ----------------------------------------------------
template <class T> template <class T>
class observable_error_impl : public ref_counted, public observable_impl<T> { template <class Step>
public: transformation<Step> observable<T>::transform(Step step) {
// -- constructors, destructors, and assignment operators -------------------- static_assert(std::is_same_v<typename Step::input_type, T>,
"step object does not match the input type");
observable_error_impl(coordinator* ctx, error what) return {*this, std::forward_as_tuple(std::move(step))};
: ctx_(ctx), what_(std::move(what)) { }
// nop
}
// -- implementation of disposable_impl --------------------------------------
void ref_disposable() const noexcept final {
this->ref();
}
void deref_disposable() const noexcept final {
this->deref();
}
void dispose() override { // -- observable::take ---------------------------------------------------------
// nop
}
bool disposed() const noexcept override { template <class T>
return true; transformation<limit_step<T>> observable<T>::take(size_t n) {
} return {*this, std::forward_as_tuple(limit_step<T>{n})};
}
// -- implementation of observable<T>::impl ---------------------------------- // -- observable::filter -------------------------------------------------------
coordinator* ctx() const noexcept final {
return ctx_;
}
disposable subscribe(observer<T> what) override {
what.on_error(what_);
return {};
}
private:
coordinator* ctx_;
error what_;
};
// -- broadcasting -------------------------------------------------------------
/// Broadcasts its input to all observers without modifying it.
template <class Step, class... Steps>
class broadcaster_impl
: public processor_impl_base<typename Step::input_type,
steps_output_type_t<Step, Steps...>> {
public:
// -- member types -----------------------------------------------------------
using input_type = typename Step::input_type;
using output_type = steps_output_type_t<Step, Steps...>;
using super = processor_impl_base<input_type, output_type>;
// -- friends ----------------------------------------------------------------
CAF_INTRUSIVE_PTR_FRIENDS(broadcaster_impl)
// -- constructors, destructors, and assignment operators --------------------
template <class... Ts>
explicit broadcaster_impl(coordinator* ctx, Ts&&... step_args)
: super(ctx), steps_(std::forward<Ts>(step_args)...) {
// nop
}
// -- implementation of disposable_impl --------------------------------------
void dispose() override {
CAF_LOG_TRACE("");
term_.dispose();
}
bool disposed() const noexcept override {
return !term_.active();
}
// -- implementation of observer<T>::impl ------------------------------------
void on_subscribe(subscription sub) override {
if (term_.start(sub))
sub_ = std::move(sub);
}
void on_next(const input_type& item) override {
if (!term_.finalized()) {
auto f = [this, &item](auto& step, auto&... steps) {
return step.on_next(item, steps..., term_);
};
auto still_running = std::apply(f, steps_);
term_.push();
if (!still_running && sub_) {
CAF_ASSERT(!term_.active());
sub_.cancel();
sub_ = nullptr;
}
}
}
void on_complete() override {
if (term_.active()) {
auto f = [this](auto& step, auto&... steps) {
step.on_complete(steps..., term_);
};
std::apply(f, steps_);
sub_ = nullptr;
}
}
void on_error(const error& what) override {
if (term_.active()) {
auto f = [this, &what](auto& step, auto&... steps) {
step.on_error(what, steps..., term_);
};
std::apply(f, steps_);
sub_ = nullptr;
}
}
// -- implementation of subscription::listener -------------------------------
void on_request(disposable_impl* sink, size_t n) override {
CAF_LOG_TRACE(CAF_ARG(n));
term_.on_request(sub_, sink, n);
}
void on_cancel(disposable_impl* sink) override {
CAF_LOG_TRACE("");
term_.on_cancel(sub_, sink);
}
// -- implementation of observable<T>::impl ----------------------------------
disposable subscribe(observer<output_type> sink) override {
return term_.add(this, sink);
}
// -- properties -------------------------------------------------------------
size_t buffered() const noexcept {
return term_.buffered();
}
observable_state state() const noexcept {
return term_.state();
}
const error& err() const noexcept {
return term_.err();
}
protected:
/// Allows us to request more items.
subscription sub_;
/// The processing steps that we apply before pushing data downstream.
std::tuple<Step, Steps...> steps_;
/// Pushes data to the observers.
broadcast_step<output_type> term_;
};
/// @relates broadcaster_impl
template <class Step, class... Steps>
using broadcaster_impl_ptr = intrusive_ptr<broadcaster_impl<Step, Steps...>>;
/// @relates broadcaster_impl
template <class T> template <class T>
broadcaster_impl_ptr<identity_step<T>> make_broadcaster_impl(coordinator* ctx) { template <class Predicate>
return make_counted<broadcaster_impl<identity_step<T>>>(ctx); transformation<filter_step<Predicate>>
} observable<T>::filter(Predicate predicate) {
using step_type = filter_step<Predicate>;
/// @relates broadcaster_impl static_assert(std::is_same_v<typename step_type::input_type, T>,
template <class Step, class... Steps> "predicate does not match the input type");
broadcaster_impl_ptr<Step, Steps...> return {*this, std::forward_as_tuple(step_type{std::move(predicate)})};
make_broadcaster_impl_from_tuple(coordinator* ctx,
std::tuple<Step, Steps...>&& tup) {
auto f = [ctx](Step&& step, Steps&&... steps) {
return make_counted<broadcaster_impl<Step, Steps...>>(ctx, std::move(step),
std::move(steps)...);
};
return std::apply(f, std::move(tup));
} }
// -- transformation ----------------------------------------------------------- // -- observable::take_while ---------------------------------------------------
/// A special type of observer that applies a series of transformation steps to
/// its input before broadcasting the result as output.
template <class Step, class... Steps>
class transformation final
: public observable_def<steps_output_type_t<Step, Steps...>> {
public:
using input_type = typename Step::input_type;
using output_type = steps_output_type_t<Step, Steps...>;
template <class Tuple>
transformation(observable<input_type> source, Tuple&& steps)
: source_(std::move(source)), steps_(std::move(steps)) {
// nop
}
transformation() = delete;
transformation(const transformation&) = delete;
transformation& operator=(const transformation&) = delete;
transformation(transformation&&) = default;
transformation& operator=(transformation&&) = default;
/// @copydoc observable::transform
template <class NewStep>
transformation<Step, Steps..., NewStep> transform(NewStep step) && {
return {std::move(source_),
std::tuple_cat(std::move(steps_),
std::make_tuple(std::move(step)))};
}
auto take(size_t n) && {
return std::move(*this).transform(limit_step<output_type>{n});
}
template <class Predicate>
auto filter(Predicate predicate) && {
return std::move(*this).transform(
filter_step<Predicate>{std::move(predicate)});
}
template <class F>
auto map(F f) && {
return std::move(*this).transform(map_step<F>{std::move(f)});
}
template <class F>
auto flat_map_optional(F f) && {
return std::move(*this).transform(flat_map_optional_step<F>{std::move(f)});
}
template <class F>
auto do_on_complete(F f) && {
return std::move(*this) //
.transform(do_on_complete_step<output_type, F>{std::move(f)});
}
template <class F>
auto do_on_error(F f) && {
return std::move(*this) //
.transform(do_on_error_step<output_type, F>{std::move(f)});
}
template <class F>
auto do_finally(F f) && {
return std::move(*this) //
.transform(do_finally_step<output_type, F>{std::move(f)});
}
auto on_error_complete() {
return std::move(*this) //
.transform(on_error_complete_step<output_type>{});
}
observable<output_type> as_observable() && {
auto pimpl = make_broadcaster_impl_from_tuple(source_.ptr()->ctx(),
std::move(steps_));
auto res = pimpl->as_observable();
source_.subscribe(observer<input_type>{std::move(pimpl)});
source_ = nullptr;
return res;
}
private:
observable<input_type> source_;
std::tuple<Step, Steps...> steps_;
};
// -- observable::transform ----------------------------------------------------
template <class T> template <class T>
template <class Step> template <class Predicate>
transformation<Step> observable<T>::transform(Step step) { transformation<take_while_step<Predicate>>
static_assert(std::is_same_v<typename Step::input_type, T>, observable<T>::take_while(Predicate predicate) {
"step object does not match the input type"); using step_type = take_while_step<Predicate>;
return {*this, std::forward_as_tuple(std::move(step))}; static_assert(std::is_same_v<typename step_type::input_type, T>,
"predicate does not match the input type");
return {*this, std::forward_as_tuple(step_type{std::move(predicate)})};
} }
// -- observable::take --------------------------------------------------------- // -- observable::sum ----------------------------------------------------------
template <class T> template <class T>
transformation<limit_step<T>> observable<T>::take(size_t n) { template <class Reducer>
return {*this, std::forward_as_tuple(limit_step<T>{n})}; transformation<reduce_step<T, Reducer>>
observable<T>::reduce(T init, Reducer reducer) {
return {*this, reduce_step<T, Reducer>{init, reducer}};
} }
// -- observable::filter ------------------------------------------------------- // -- observable::distinct -----------------------------------------------------
template <class T> template <class T>
template <class Predicate> transformation<distinct_step<T>> observable<T>::distinct() {
transformation<filter_step<Predicate>> return {*this, distinct_step<T>{}};
observable<T>::filter(Predicate predicate) {
using step_type = filter_step<Predicate>;
static_assert(std::is_same_v<typename step_type::input_type, T>,
"predicate does not match the input type");
return {*this, std::forward_as_tuple(step_type{std::move(predicate)})};
} }
// -- observable::map ---------------------------------------------------------- // -- observable::map ----------------------------------------------------------
...@@ -895,922 +314,136 @@ template <class T> ...@@ -895,922 +314,136 @@ template <class T>
template <class OnNext> template <class OnNext>
disposable observable<T>::for_each(OnNext on_next) { disposable observable<T>::for_each(OnNext on_next) {
auto obs = make_observer(std::move(on_next)); auto obs = make_observer(std::move(on_next));
subscribe(observer<T>{obs}); return subscribe(std::move(obs));
return std::move(obs).as_disposable();
} }
template <class T> template <class T>
template <class OnNext, class OnError> template <class OnNext, class OnError>
disposable observable<T>::for_each(OnNext on_next, OnError on_error) { disposable observable<T>::for_each(OnNext on_next, OnError on_error) {
auto obs = make_observer(std::move(on_next), std::move(on_error)); auto obs = make_observer(std::move(on_next), std::move(on_error));
subscribe(obs); return subscribe(std::move(obs));
return std::move(obs).as_disposable();
}
template <class T>
template <class OnNext, class OnError, class OnComplete>
disposable observable<T>::for_each(OnNext on_next, OnError on_error,
OnComplete on_complete) {
auto obs = make_observer(std::move(on_next), std::move(on_error),
std::move(on_complete));
subscribe(obs);
return std::move(obs).as_disposable();
}
// -- observable::flat_map -----------------------------------------------------
/// @relates merger_impl
template <class T>
struct merger_input {
explicit merger_input(observable<T> in) : in(std::move(in)) {
// nop
}
/// Stores a handle to the input observable for delayed subscription.
observable<T> in;
/// The subscription to this input.
subscription sub;
/// Stores received items until the merger can forward them downstream.
std::vector<T> buf;
};
/// Combines items from any number of observables.
template <class T>
class merger_impl : public observable_impl_base<T> {
public:
// -- member types -----------------------------------------------------------
using super = observable_impl_base<T>;
using input_t = merger_input<T>;
using input_ptr = std::unique_ptr<input_t>;
using input_key = size_t;
using input_map = detail::unordered_flat_map<input_key, input_ptr>;
// -- friends ----------------------------------------------------------------
CAF_INTRUSIVE_PTR_FRIENDS(merger_impl)
template <class, class, class>
friend class flow::forwarder;
// -- constructors, destructors, and assignment operators --------------------
explicit merger_impl(coordinator* ctx) : super(ctx) {
// nop
}
// -- implementation of disposable_impl --------------------------------------
void dispose() override {
for (auto& kvp : inputs_) {
auto& input = *kvp.second;
if (input.in) {
input.in = nullptr;
}
if (input.sub) {
input.sub.cancel();
input.sub = nullptr;
}
}
inputs_.clear();
term_.dispose();
}
bool disposed() const noexcept override {
return term_.finalized();
}
// -- implementation of observable<T>::impl ----------------------------------
void on_request(disposable_impl* sink, size_t demand) override {
if (auto n = term_.on_request(sink, demand); n > 0) {
pull(n);
term_.push();
}
}
void on_cancel(disposable_impl* sink) override {
if (auto n = term_.on_cancel(sink); n > 0) {
pull(n);
term_.push();
}
}
disposable subscribe(observer<T> sink) override {
// On the first subscribe, we subscribe to our inputs unless the user did
// not add any inputs before that. In that case, we close immediately except
// when running with shutdown_on_last_complete turned off.
if (term_.idle() && inputs_.empty() && flags_.shutdown_on_last_complete)
term_.close();
auto res = term_.add(this, sink);
if (res && term_.start()) {
for (auto& [key, input] : inputs_) {
using fwd_impl = forwarder<T, merger_impl, size_t>;
auto fwd = make_counted<fwd_impl>(this, key);
input->in.subscribe(fwd->as_observer());
}
}
return res;
}
// -- dynamic input management -----------------------------------------------
template <class Observable>
void add(Observable source) {
switch (term_.state()) {
case observable_state::idle:
// Only add to the inputs but don't subscribe yet.
emplace(std::move(source).as_observable());
break;
case observable_state::running: {
// Add and subscribe.
auto& [key, input] = emplace(std::move(source).as_observable());
using fwd_impl = forwarder<T, merger_impl, size_t>;
auto fwd = make_counted<fwd_impl>(this, key);
input->in.subscribe(fwd->as_observer());
break;
}
default:
// In any other case, this turns into a no-op.
break;
}
}
// -- properties -------------------------------------------------------------
size_t buffered() {
return std::accumulate(inputs_.begin(), inputs_.end(), size_t{0},
[](size_t tmp, const merger_input<T>& in) {
return tmp + in.buf.size();
});
}
bool shutdown_on_last_complete() const noexcept {
return flags_.shutdown_on_last_complete;
}
void shutdown_on_last_complete(bool new_value) noexcept {
flags_.shutdown_on_last_complete = new_value;
if (new_value && inputs_.empty())
term_.fin();
}
private:
typename input_map::value_type& emplace(observable<T> source) {
auto& vec = inputs_.container();
vec.emplace_back(next_key_++, std::make_unique<input_t>(std::move(source)));
return vec.back();
}
void fwd_on_subscribe(input_key key, subscription sub) {
if (!term_.finalized()) {
if (auto i = inputs_.find(key); i != inputs_.end()) {
auto& in = *i->second;
sub.request(max_pending_);
in.sub = std::move(sub);
} else {
sub.cancel();
}
} else {
sub.cancel();
}
}
void drop_if_empty(typename input_map::iterator i) {
auto& in = *i->second;
if (in.buf.empty()) {
inputs_.erase(i);
if (inputs_.empty() && flags_.shutdown_on_last_complete)
term_.fin();
} else {
in.in = nullptr;
in.sub = nullptr;
}
}
void fwd_on_complete(input_key key) {
if (auto i = inputs_.find(key); i != inputs_.end())
drop_if_empty(i);
}
void fwd_on_error(input_key key, const error& what) {
if (auto i = inputs_.find(key); i != inputs_.end()) {
if (!term_.err()) {
term_.err(what);
if (flags_.delay_error) {
drop_if_empty(i);
} else {
auto& in = *i->second;
if (!in.buf.empty()) {
for (auto& item : in.buf)
term_.on_next(item);
}
term_.fin();
for (auto j = inputs_.begin(); j != inputs_.end(); ++j)
if (j != i && j->second->sub)
j->second->sub.cancel();
inputs_.clear();
}
} else {
drop_if_empty(i);
}
}
}
void fwd_on_next(input_key key, const T& item) {
if (auto i = inputs_.find(key); i != inputs_.end()) {
auto& in = *i->second;
if (!term_.finalized()) {
if (term_.min_demand() > 0) {
term_.on_next(item);
term_.push();
if (in.sub)
in.sub.request(1);
} else {
in.buf.emplace_back(item);
}
}
}
}
void pull(size_t n) {
// Must not be re-entered. Any on_request call must use the event loop.
CAF_ASSERT(!pulling_);
if (inputs_.empty()) {
if (flags_.shutdown_on_last_complete)
term_.fin();
return;
}
CAF_DEBUG_STMT(pulling_ = true);
auto& in_vec = inputs_.container();
for (size_t i = 0; n > 0 && i < inputs_.size(); ++i) {
auto index = (pos_ + 1) % inputs_.size();
auto& in = *in_vec[index].second;
if (auto m = std::min(in.buf.size(), n); m > 0) {
n -= m;
auto items = make_span(in.buf.data(), m);
for (auto& item : items)
term_.on_next(item);
in.buf.erase(in.buf.begin(), in.buf.begin() + m);
if (in.sub) {
in.sub.request(m);
pos_ = index;
} else if (!in.in && in.buf.empty()) {
in_vec.erase(in_vec.begin() + index);
if (in_vec.empty()) {
if (flags_.shutdown_on_last_complete)
term_.fin();
CAF_DEBUG_STMT(pulling_ = false);
return;
}
} else {
pos_ = index;
}
}
}
CAF_DEBUG_STMT(pulling_ = false);
}
struct flags_t {
bool delay_error : 1;
bool shutdown_on_last_complete : 1;
flags_t() : delay_error(false), shutdown_on_last_complete(true) {
// nop
}
};
/// Fine-tunes the behavior of the merger.
flags_t flags_;
/// Configures how many items we buffer per input.
size_t max_pending_ = defaults::flow::buffer_size;
/// Stores the last round-robin read position.
size_t pos_ = 0;
/// Associates inputs with ascending keys.
input_map inputs_;
/// Stores the last round-robin read position.
size_t next_key_ = 0;
/// Pushes items to subscribed observers.
broadcast_step<T> term_;
#ifdef CAF_ENABLE_RUNTIME_CHECKS
/// Protect against re-entering `pull`.
bool pulling_ = false;
#endif
};
template <class T>
using merger_impl_ptr = intrusive_ptr<merger_impl<T>>;
template <class T, class F>
class flat_map_observer_impl : public ref_counted, public observer_impl<T> {
public:
using mapped_type = decltype((std::declval<F&>())(std::declval<const T&>()));
using inner_type = typename mapped_type::output_type;
CAF_INTRUSIVE_PTR_FRIENDS(flat_map_observer_impl)
flat_map_observer_impl(coordinator* ctx, F f)
: ctx_(ctx), map_(std::move(f)) {
merger_.emplace(ctx);
merger_->shutdown_on_last_complete(false);
}
void dispose() override {
if (sub_) {
sub_.cancel();
sub_ = nullptr;
merger_->shutdown_on_last_complete(true);
merger_ = nullptr;
}
}
bool disposed() const noexcept override {
return merger_ != nullptr;
}
void ref_disposable() const noexcept final {
this->ref();
}
void deref_disposable() const noexcept final {
this->deref();
}
void on_complete() override {
if (sub_) {
sub_ = nullptr;
merger_->shutdown_on_last_complete(true);
merger_ = nullptr;
}
}
void on_error(const error& what) override {
if (sub_) {
sub_ = nullptr;
merger_->shutdown_on_last_complete(true);
auto obs = make_observable<observable_error_impl<inner_type>>(ctx_, what);
merger_->add(std::move(obs));
merger_ = nullptr;
}
}
void on_subscribe(subscription sub) override {
if (!sub_ && merger_) {
sub_ = std::move(sub);
sub_.request(10);
} else {
sub.cancel();
}
}
void on_next(const T& src) override {
if (sub_) {
merger_->add(map_(src).as_observable());
sub_.request(1);
}
}
observable<inner_type> merger() {
return observable<inner_type>{merger_};
}
auto& merger_ptr() {
return merger_;
}
private:
coordinator* ctx_;
subscription sub_;
F map_;
intrusive_ptr<merger_impl<inner_type>> merger_;
};
template <class T>
template <class F>
auto observable<T>::flat_map(F f) {
using f_res = decltype(f(std::declval<const T&>()));
static_assert(is_observable_v<f_res>,
"mapping functions must return an observable");
using impl_t = flat_map_observer_impl<T, F>;
auto obs = make_counted<impl_t>(pimpl_->ctx(), std::move(f));
pimpl_->subscribe(obs->as_observer());
return obs->merger();
}
// -- observable::flat_map_optional --------------------------------------------
template <class T>
template <class F>
transformation<flat_map_optional_step<F>>
observable<T>::flat_map_optional(F f) {
using step_type = flat_map_optional_step<F>;
static_assert(std::is_same_v<typename step_type::input_type, T>,
"flat_map_optional function does not match the input type");
return {*this, std::forward_as_tuple(step_type{std::move(f)})};
}
// -- observable::concat_map ---------------------------------------------------
/// Combines items from any number of observables.
template <class T>
class concat_impl : public observable_impl_base<T> {
public:
// -- member types -----------------------------------------------------------
using super = observable_impl_base<T>;
using input_key = size_t;
// -- friends ----------------------------------------------------------------
CAF_INTRUSIVE_PTR_FRIENDS(concat_impl)
template <class, class, class>
friend class flow::forwarder;
// -- constructors, destructors, and assignment operators --------------------
explicit concat_impl(coordinator* ctx) : super(ctx) {
// nop
}
// -- implementation of disposable_impl --------------------------------------
void dispose() override {
if (sub_)
sub_.cancel();
inputs_.clear();
term_.dispose();
}
bool disposed() const noexcept override {
return term_.finalized();
}
// -- implementation of subscription::listener -------------------------------
void on_request(disposable_impl* sink, size_t demand) override {
if (auto n = term_.on_request(sink, demand); n > 0) {
in_flight_ += n;
if (sub_)
sub_.request(n);
}
}
void on_cancel(disposable_impl* sink) override {
if (auto n = term_.on_cancel(sink); n > 0) {
in_flight_ += n;
if (sub_)
sub_.request(n);
}
}
// -- implementation of observable<T>::impl ----------------------------------
disposable subscribe(observer<T> sink) override {
// On the first subscribe, we subscribe to our inputs unless the user did
// not add any inputs before that. In that case, we close immediately except
// when running with shutdown_on_last_complete turned off.
if (term_.idle() && inputs_.empty() && flags_.shutdown_on_last_complete)
term_.close();
auto res = term_.add(this, sink);
if (res && term_.start() && !inputs_.empty())
subscribe_next();
return res;
}
// -- dynamic input management -----------------------------------------------
template <class Observable>
void add(Observable source) {
switch (term_.state()) {
case observable_state::idle:
inputs_.emplace_back(std::move(source).as_observable());
break;
case observable_state::running:
inputs_.emplace_back(std::move(source).as_observable());
if (inputs_.size() == 1)
subscribe_next();
break;
default:
// In any other case, this turns into a no-op.
break;
}
}
// -- properties -------------------------------------------------------------
bool shutdown_on_last_complete() const noexcept {
return flags_.shutdown_on_last_complete;
}
void shutdown_on_last_complete(bool new_value) noexcept {
flags_.shutdown_on_last_complete = new_value;
if (new_value && inputs_.empty())
term_.fin();
}
private:
void subscribe_next() {
CAF_ASSERT(!inputs_.empty());
CAF_ASSERT(!sub_);
auto input = inputs_.front();
++in_key_;
using fwd_impl = forwarder<T, concat_impl, size_t>;
auto fwd = make_counted<fwd_impl>(this, in_key_);
input.subscribe(fwd->as_observer());
}
void fwd_on_subscribe(input_key key, subscription sub) {
if (in_key_ == key && term_.active()) {
sub_ = std::move(sub);
if (in_flight_ > 0)
sub_.request(in_flight_);
} else {
sub.cancel();
}
}
void fwd_on_complete(input_key key) {
if (in_key_ == key) {
inputs_.erase(inputs_.begin());
sub_ = nullptr;
if (!inputs_.empty()) {
subscribe_next();
} else if (flags_.shutdown_on_last_complete) {
term_.fin();
}
}
}
void fwd_on_error(input_key key, const error& what) {
if (in_key_ == key) {
if (!flags_.delay_error) {
term_.on_error(what);
sub_ = nullptr;
inputs_.clear();
} else if (!term_.err()) {
term_.err(what);
fwd_on_complete(key);
} else {
fwd_on_complete(key);
}
}
}
void fwd_on_next(input_key key, const T& item) {
if (in_key_ == key && !term_.finalized()) {
CAF_ASSERT(in_flight_ >= 0);
--in_flight_;
term_.on_next(item);
term_.push();
}
}
struct flags_t {
bool delay_error : 1;
bool shutdown_on_last_complete : 1;
flags_t() : delay_error(false), shutdown_on_last_complete(true) {
// nop
}
};
/// Fine-tunes the behavior of the concat.
flags_t flags_;
/// Stores our input sources. The first input is active (subscribed to) while
/// the others are pending (not subscribed to).
std::vector<observable<T>> inputs_;
/// Our currently active subscription.
subscription sub_;
/// Identifies the forwarder.
input_key in_key_ = 0;
/// Stores how much demand we have left. When switching to a new input, we
/// pass any demand unused by the previous input to the new one.
size_t in_flight_ = 0;
/// Pushes items to subscribed observers.
broadcast_step<T> term_;
};
template <class T, class F>
class concat_map_observer_impl : public ref_counted, public observer_impl<T> {
public:
using mapped_type = decltype((std::declval<F&>())(std::declval<const T&>()));
using inner_type = typename mapped_type::output_type;
CAF_INTRUSIVE_PTR_FRIENDS(concat_map_observer_impl)
concat_map_observer_impl(coordinator* ctx, F f)
: ctx_(ctx), map_(std::move(f)) {
concat_.emplace(ctx);
concat_->shutdown_on_last_complete(false);
}
void dispose() override {
if (sub_) {
sub_.cancel();
sub_ = nullptr;
concat_->shutdown_on_last_complete(true);
concat_ = nullptr;
}
}
bool disposed() const noexcept override {
return concat_ != nullptr;
}
void ref_disposable() const noexcept final {
this->ref();
}
void deref_disposable() const noexcept final {
this->deref();
}
void on_complete() override {
if (sub_) {
sub_ = nullptr;
concat_->shutdown_on_last_complete(true);
concat_ = nullptr;
}
}
void on_error(const error& what) override {
if (sub_) {
sub_ = nullptr;
concat_->shutdown_on_last_complete(true);
auto obs = make_observable<observable_error_impl<inner_type>>(ctx_, what);
concat_->add(std::move(obs));
concat_ = nullptr;
}
}
void on_subscribe(subscription sub) override {
if (!sub_ && concat_) {
sub_ = std::move(sub);
sub_.request(10);
} else {
sub.cancel();
}
}
void on_next(const T& src) override {
if (sub_) {
concat_->add(map_(src).as_observable());
sub_.request(1);
}
}
observable<inner_type> concat() {
return observable<inner_type>{concat_};
}
auto& concat_ptr() {
return concat_;
}
private:
coordinator* ctx_;
subscription sub_;
F map_;
intrusive_ptr<concat_impl<inner_type>> concat_;
};
template <class T>
template <class F>
auto observable<T>::concat_map(F f) {
using f_res = decltype(f(std::declval<const T&>()));
static_assert(is_observable_v<f_res>,
"mapping functions must return an observable");
using impl_t = concat_map_observer_impl<T, F>;
auto obs = make_counted<impl_t>(pimpl_->ctx(), std::move(f));
pimpl_->subscribe(obs->as_observer());
return obs->concat();
} }
// -- observable::prefix_and_tail ----------------------------------------------
template <class T> template <class T>
class prefix_and_tail_observable_impl final template <class OnNext, class OnError, class OnComplete>
: public ref_counted, disposable observable<T>::for_each(OnNext on_next, OnError on_error,
public subscription::listener, OnComplete on_complete) {
public observable_impl<T>, // For the forwarding to the 'tail'. auto obs = make_observer(std::move(on_next), std::move(on_error),
public processor_impl<T, cow_tuple<std::vector<T>, observable<T>>> { std::move(on_complete));
public: return subscribe(std::move(obs));
// -- member types ----------------------------------------------------------- }
using in_t = T;
using out_t = cow_tuple<std::vector<T>, observable<T>>;
using in_obs_t = observable_impl<in_t>;
using out_obs_t = observable_impl<out_t>;
// -- constructors, destructors, and assignment operators --------------------
prefix_and_tail_observable_impl(coordinator* ctx, size_t prefix_size)
: ctx_(ctx), prefix_size_(prefix_size) {
// nop
}
// -- friends ----------------------------------------------------------------
CAF_INTRUSIVE_PTR_FRIENDS(prefix_and_tail_observable_impl)
// -- implementation of disposable_impl --------------------------------------
void dispose() override {
if (sub_) {
sub_.cancel();
sub_ = nullptr;
}
if (obs_) {
obs_.on_complete();
obs_ = nullptr;
}
if (tail_) {
tail_.on_complete();
tail_ = nullptr;
}
}
bool disposed() const noexcept override {
return !sub_ && !obs_ && !tail_;
}
void ref_disposable() const noexcept final {
this->ref();
}
void deref_disposable() const noexcept final {
this->deref();
}
// -- implementation of observable<T>::impl ----------------------------------
coordinator* ctx() const noexcept final {
return ctx_;
}
// -- implementation of observable<in_t>::impl ------------------------------- // -- observable::merge --------------------------------------------------------
disposable subscribe(observer<in_t> sink) override { template <class T>
if (sink.ptr() == tail_.ptr()) { template <class... Inputs>
using sub_t = subscription::default_impl; auto observable<T>::merge(Inputs&&... xs) {
sink.on_subscribe(sub_t::make_unsafe(ctx_, this, sink.ptr())); if constexpr (is_observable_v<output_type>) {
return sink.as_disposable(); using value_t = output_type_t<output_type>;
using impl_t = op::merge<value_t>;
return make_observable<impl_t>(ctx(), *this, std::forward<Inputs>(xs)...);
} else { } else {
sink.on_error(make_error(sec::invalid_observable)); static_assert(
return disposable{}; sizeof...(Inputs) > 0,
} "merge without arguments expects this observable to emit observables");
using impl_t = op::merge<output_type>;
return make_observable<impl_t>(ctx(), *this, std::forward<Inputs>(xs)...);
} }
}
// -- implementation of observable<out_t>::impl ------------------------------ // -- observable::flat_map -----------------------------------------------------
disposable subscribe(observer<out_t> sink) override { template <class T>
obs_ = sink; template <class F>
using sub_t = subscription::default_impl; auto observable<T>::flat_map(F f) {
sink.on_subscribe(sub_t::make_unsafe(ctx_, this, sink.ptr())); using res_t = decltype(f(std::declval<const output_type&>()));
return sink.as_disposable(); if constexpr (is_observable_v<res_t>) {
return map([fn = std::move(f)](const output_type& x) mutable {
return fn(x).as_observable();
})
.merge();
} else if constexpr (detail::is_optional_v<res_t>) {
return map([fn = std::move(f)](const output_type& x) mutable {
return fn(x);
})
.filter([](const res_t& x) { return x.has_value(); })
.map([](const res_t& x) { return *x; });
} else {
// Here, we dispatch to concat() instead of merging the containers. Merged
// output is probably not what anyone would expect and since the values are
// all available immediately, there is no good reason to mess up the emitted
// order of values.
static_assert(detail::is_iterable_v<res_t>);
return map([cptr = ctx(), fn = std::move(f)](const output_type& x) mutable {
return cptr->make_observable().from_container(fn(x));
})
.concat();
} }
}
// -- implementation of subscription::listener ------------------------------- // -- observable::flat_map_optional --------------------------------------------
void on_request(disposable_impl* sink, size_t n) override {
if (sink == obs_.ptr()) {
if (sub_ && !requested_prefix_) {
requested_prefix_ = true;
sub_.request(prefix_size_);
}
} else if (sink == tail_.ptr()) {
if (sub_)
sub_.request(n);
}
}
void on_cancel(disposable_impl* sink) override { template <class T>
if (sink == obs_.ptr()) { template <class F>
// Only has an effect when canceling immediately. Otherwise, we forward to transformation<flat_map_optional_step<F>>
// tail_ and the original observer no longer is of any interest since it observable<T>::flat_map_optional(F f) {
// receives at most one item anyways. using step_type = flat_map_optional_step<F>;
if (sub_ && !tail_) { static_assert(std::is_same_v<typename step_type::input_type, T>,
sub_.cancel(); "flat_map_optional function does not match the input type");
sub_ = nullptr; return {*this, std::forward_as_tuple(step_type{std::move(f)})};
} }
} else if (sink == tail_.ptr()) {
if (sub_) {
sub_.cancel();
sub_ = nullptr;
}
}
}
// -- implementation of observer<in_t>::impl --------------------------------- // -- observable::concat -------------------------------------------------------
void on_subscribe(subscription sub) override { template <class T>
if (!had_subscriber_) { template <class... Inputs>
had_subscriber_ = true; auto observable<T>::concat(Inputs&&... xs) {
sub_ = std::move(sub); if constexpr (is_observable_v<output_type>) {
using value_t = output_type_t<output_type>;
using impl_t = op::concat<value_t>;
return make_observable<impl_t>(ctx(), *this, std::forward<Inputs>(xs)...);
} else { } else {
sub.cancel(); static_assert(
} sizeof...(Inputs) > 0,
} "merge without arguments expects this observable to emit observables");
using impl_t = op::concat<output_type>;
void on_next(const in_t& item) override { return make_observable<impl_t>(ctx(), *this, std::forward<Inputs>(xs)...);
if (tail_) {
tail_.on_next(item);
} else if (obs_) {
prefix_.emplace_back(item);
if (prefix_.size() >= prefix_size_) {
auto tptr = make_broadcaster_impl<in_t>(ctx_);
tail_ = tptr->as_observer();
static_cast<observable_impl<in_t>*>(this)->subscribe(tail_);
auto tup = make_cow_tuple(std::move(prefix_), tptr->as_observable());
obs_.on_next(tup);
obs_.on_complete();
obs_ = nullptr;
}
}
} }
}
void on_complete() override { // -- observable::concat_map ---------------------------------------------------
sub_ = nullptr;
if (obs_) {
obs_.on_complete();
obs_ = nullptr;
}
if (tail_) {
tail_.on_complete();
tail_ = nullptr;
}
}
void on_error(const error& what) override { template <class T>
sub_ = nullptr; template <class F>
if (obs_) { auto observable<T>::concat_map(F f) {
obs_.on_error(what); using res_t = decltype(f(std::declval<const output_type&>()));
obs_ = nullptr; if constexpr (is_observable_v<res_t>) {
} return map([fn = std::move(f)](const output_type& x) mutable {
if (tail_) { return fn(x).as_observable();
tail_.on_error(what); })
tail_ = nullptr; .concat();
} } else if constexpr (detail::is_optional_v<res_t>) {
return map([fn = std::move(f)](const output_type& x) mutable {
return fn(x);
})
.filter([](const res_t& x) { return x.has_value(); })
.map([](const res_t& x) { return *x; });
} else {
static_assert(detail::is_iterable_v<res_t>);
return map([cptr = ctx(), fn = std::move(f)](const output_type& x) mutable {
return cptr->make_observable().from_container(fn(x));
})
.concat();
} }
}
private: // -- observable::prefix_and_tail ----------------------------------------------
/// Our context.
coordinator* ctx_;
/// Subscription to the input data.
subscription sub_;
/// Handle for the observer that gets the prefix + tail tuple.
observer<out_t> obs_;
/// Handle to the tail for forwarding any data after the prefix.
observer<in_t> tail_;
/// Makes sure we only respect the first subscriber.
bool had_subscriber_ = false;
/// Stores whether we have requested the prefix;
bool requested_prefix_ = false;
/// Buffer for storing the prefix elements.
std::vector<in_t> prefix_;
/// User-defined size of the prefix.
size_t prefix_size_;
};
template <class T> template <class T>
observable<cow_tuple<std::vector<T>, observable<T>>> observable<cow_tuple<std::vector<T>, observable<T>>>
observable<T>::prefix_and_tail(size_t prefix_size) { observable<T>::prefix_and_tail(size_t prefix_size) {
using impl_t = prefix_and_tail_observable_impl<T>; using impl_t = op::prefix_and_tail<T>;
using out_t = cow_tuple<std::vector<T>, observable<T>>; return make_observable<impl_t>(ctx(), pimpl_, prefix_size);
auto obs = make_counted<impl_t>(pimpl_->ctx(), prefix_size);
pimpl_->subscribe(obs->as_observer());
return static_cast<observable_impl<out_t>*>(obs.get())->as_observable();
} }
// -- observable::prefix_and_tail ---------------------------------------------- // -- observable::prefix_and_tail ----------------------------------------------
...@@ -1831,157 +464,6 @@ observable<cow_tuple<T, observable<T>>> observable<T>::head_and_tail() { ...@@ -1831,157 +464,6 @@ observable<cow_tuple<T, observable<T>>> observable<T>::head_and_tail() {
/// Reads from an observable buffer and emits the consumed items. /// Reads from an observable buffer and emits the consumed items.
/// @note Only supports a single observer. /// @note Only supports a single observer.
template <class Buffer>
class observable_buffer_impl
: public observable_impl_base<typename Buffer::value_type>,
public async::consumer {
public:
// -- member types -----------------------------------------------------------
using value_type = typename Buffer::value_type;
using buffer_ptr = intrusive_ptr<Buffer>;
using super = observable_impl_base<value_type>;
// -- friends ----------------------------------------------------------------
CAF_INTRUSIVE_PTR_FRIENDS(observable_buffer_impl)
// -- constructors, destructors, and assignment operators --------------------
observable_buffer_impl(coordinator* ctx, buffer_ptr buf)
: super(ctx), buf_(buf) {
// Unlike regular observables, we need a strong reference to the context.
// Otherwise, the buffer might call schedule_fn on a destroyed object.
this->ctx()->ref_coordinator();
}
~observable_buffer_impl() {
if (buf_)
buf_->cancel();
this->ctx()->deref_coordinator();
}
// -- implementation of disposable_impl --------------------------------------
void dispose() override {
CAF_LOG_TRACE("");
if (buf_) {
buf_->cancel();
buf_ = nullptr;
if (dst_) {
dst_.on_complete();
dst_ = nullptr;
}
}
}
bool disposed() const noexcept override {
return buf_ == nullptr;
}
// -- implementation of observable<T>::impl ----------------------------------
void on_request(disposable_impl*, size_t n) override {
CAF_LOG_TRACE(CAF_ARG(n));
demand_ += n;
if (demand_ == n)
pull();
}
void on_cancel(disposable_impl*) override {
CAF_LOG_TRACE("");
dst_ = nullptr;
dispose();
}
disposable subscribe(observer<value_type> what) override {
CAF_LOG_TRACE("");
if (buf_ && !dst_) {
CAF_LOG_DEBUG("add destination");
dst_ = std::move(what);
return super::do_subscribe(dst_.ptr());
} else {
CAF_LOG_DEBUG("already have a destination");
auto err = make_error(sec::cannot_add_upstream,
"observable buffers support only one observer");
what.on_error(err);
return disposable{};
}
}
// -- implementation of async::consumer: these may get called concurrently ---
void on_producer_ready() override {
// nop
}
void on_producer_wakeup() override {
CAF_LOG_TRACE("");
this->ctx()->schedule_fn([ptr{strong_ptr()}] {
CAF_LOG_TRACE("");
ptr->pull();
});
}
void ref_consumer() const noexcept override {
this->ref();
}
void deref_consumer() const noexcept override {
this->deref();
}
protected:
coordinator* ctx_;
private:
void pull() {
CAF_LOG_TRACE("");
if (!buf_ || pulling_ || !dst_)
return;
pulling_ = true;
struct decorator {
size_t* demand;
typename observer<value_type>::impl* dst;
void on_next(const value_type& item) {
CAF_LOG_TRACE(CAF_ARG(item));
CAF_ASSERT(*demand > 0);
--*demand;
dst->on_next(item);
}
void on_complete() {
CAF_LOG_TRACE("");
dst->on_complete();
}
void on_error(const error& what) {
CAF_LOG_TRACE(CAF_ARG(what));
dst->on_error(what);
}
};
decorator dst{&demand_, dst_.ptr()};
if (!buf_->pull(async::prioritize_errors, demand_, dst).first) {
buf_ = nullptr;
dst_ = nullptr;
}
pulling_ = false;
}
intrusive_ptr<observable_buffer_impl> strong_ptr() {
return {this};
}
buffer_ptr buf_;
/// Stores a pointer to the target observer running on `remote_ctx_`.
observer<value_type> dst_;
bool pulling_ = false;
size_t demand_ = 0;
};
template <class T> template <class T>
async::consumer_resource<T> async::consumer_resource<T>
observable<T>::to_resource(size_t buffer_size, size_t min_request_size) { observable<T>::to_resource(size_t buffer_size, size_t min_request_size) {
...@@ -1998,13 +480,10 @@ observable<T>::to_resource(size_t buffer_size, size_t min_request_size) { ...@@ -1998,13 +480,10 @@ observable<T>::to_resource(size_t buffer_size, size_t min_request_size) {
template <class T> template <class T>
observable<T> observable<T>::observe_on(coordinator* other, size_t buffer_size, observable<T> observable<T>::observe_on(coordinator* other, size_t buffer_size,
size_t min_request_size) { size_t min_request_size) {
using buffer_type = async::spsc_buffer<T>; auto [pull, push] = async::make_spsc_buffer_resource<T>(buffer_size,
auto buf = make_counted<buffer_type>(buffer_size, min_request_size); min_request_size);
subscribe(async::producer_resource<T>{buf}); subscribe(push);
auto adapter = make_counted<observable_buffer_impl<buffer_type>>(other, buf); return make_observable<op::from_resource<T>>(other, std::move(pull));
buf->set_consumer(adapter);
other->watch(adapter->as_disposable());
return observable<T>{std::move(adapter)};
} }
// -- observable::subscribe ---------------------------------------------------- // -- observable::subscribe ----------------------------------------------------
...@@ -2018,289 +497,13 @@ disposable observable<T>::subscribe(async::producer_resource<T> resource) { ...@@ -2018,289 +497,13 @@ disposable observable<T>::subscribe(async::producer_resource<T> resource) {
auto adapter = make_counted<adapter_type>(pimpl_->ctx(), buf); auto adapter = make_counted<adapter_type>(pimpl_->ctx(), buf);
buf->set_producer(adapter); buf->set_producer(adapter);
auto obs = adapter->as_observer(); auto obs = adapter->as_observer();
pimpl_->ctx()->watch(obs.as_disposable()); auto sub = subscribe(std::move(obs));
return subscribe(std::move(obs)); pimpl_->ctx()->watch(sub);
return sub;
} else { } else {
CAF_LOG_DEBUG("failed to open producer resource"); CAF_LOG_DEBUG("failed to open producer resource");
return {}; return {};
} }
} }
// -- custom operators ---------------------------------------------------------
/// An observable that represents an empty range. As soon as an observer
/// requests values from this observable, it calls `on_complete`.
template <class T>
class empty_observable_impl : public observable_impl_base<T> {
public:
// -- member types -----------------------------------------------------------
using super = observable_impl_base<T>;
using output_type = T;
// -- constructors, destructors, and assignment operators --------------------
explicit empty_observable_impl(coordinator* ctx) : super(ctx) {
// nop
}
// -- implementation of disposable_impl --------------------------------------
void dispose() override {
// nop
}
bool disposed() const noexcept override {
return true;
}
// -- implementation of observable<T>::impl ----------------------------------
void on_request(disposable_impl* snk, size_t) override {
static_cast<observer_impl<output_type>*>(snk)->on_complete();
}
void on_cancel(disposable_impl*) override {
// nop
}
disposable subscribe(observer<output_type> sink) override {
return this->do_subscribe(sink.ptr());
}
private:
coordinator* ctx_;
};
/// An observable that never calls any callbacks on its subscribers.
template <class T>
class mute_observable_impl : public observable_impl_base<T> {
public:
// -- member types -----------------------------------------------------------
using super = observable_impl_base<T>;
using output_type = T;
// -- constructors, destructors, and assignment operators --------------------
explicit mute_observable_impl(coordinator* ctx) : super(ctx) {
// nop
}
// -- implementation of disposable_impl --------------------------------------
void dispose() override {
if (!disposed_) {
disposed_ = true;
for (auto& obs : observers_)
obs.on_complete();
}
}
bool disposed() const noexcept override {
return disposed_;
}
// -- implementation of observable<T>::impl ----------------------------------
void on_request(disposable_impl*, size_t) override {
// nop
}
void on_cancel(disposable_impl*) override {
// nop
}
disposable subscribe(observer<output_type> sink) override {
if (!disposed_) {
auto ptr = make_counted<subscription::nop_impl>();
sink.ptr()->on_subscribe(subscription{std::move(ptr)});
observers_.emplace_back(sink);
return sink.as_disposable();
} else {
return this->reject_subscription(sink, sec::disposed);
}
}
private:
bool disposed_ = false;
std::vector<observer<output_type>> observers_;
};
/// An observable with minimal internal logic. Useful for writing unit tests.
template <class T>
class passive_observable : public observable_impl_base<T> {
public:
// -- member types -----------------------------------------------------------
using output_type = T;
using super = observable_impl_base<T>;
// -- friends ----------------------------------------------------------------
CAF_INTRUSIVE_PTR_FRIENDS(passive_observable)
// -- constructors, destructors, and assignment operators --------------------
passive_observable(coordinator* ctx) : super(ctx) {
// nop
}
// -- implementation of disposable_impl --------------------------------------
void dispose() override {
if (is_active(state)) {
demand = 0;
if (out) {
out.on_complete();
out = nullptr;
}
state = observable_state::completed;
}
}
bool disposed() const noexcept override {
return !is_active(state);
}
// -- implementation of observable_impl<T> -----------------------------------
void on_request(disposable_impl* sink, size_t n) override {
if (out.ptr() == sink) {
demand += n;
}
}
void on_cancel(disposable_impl* sink) override {
if (out.ptr() == sink) {
demand = 0;
out = nullptr;
state = observable_state::completed;
}
}
disposable subscribe(observer<output_type> sink) override {
if (state == observable_state::idle) {
state = observable_state::running;
out = sink;
return this->do_subscribe(sink.ptr());
} else if (is_final(state)) {
return this->reject_subscription(sink, sec::disposed);
} else {
return this->reject_subscription(sink, sec::too_many_observers);
}
}
// -- pushing items ----------------------------------------------------------
void push(span<const output_type> items) {
if (items.size() > demand)
CAF_RAISE_ERROR("observables must not emit more items than requested");
demand -= items.size();
for (auto& item : items)
out.on_next(item);
}
void push(const output_type& item) {
if (demand == 0)
CAF_RAISE_ERROR("observables must not emit more items than requested");
--demand;
out.on_next(item);
}
template <class... Ts>
void push(output_type x0, output_type x1, Ts... xs) {
output_type items[] = {std::move(x0), std::move(x1), std::move(xs)...};
push(make_span(items, sizeof...(Ts) + 2));
}
void complete() {
if (is_active(state)) {
demand = 0;
if (out) {
out.on_complete();
out = nullptr;
}
state = observable_state::completed;
}
}
void abort(const error& reason) {
if (is_active(state)) {
demand = 0;
if (out) {
out.on_error(reason);
out = nullptr;
}
state = observable_state::aborted;
}
}
// -- member variables -------------------------------------------------------
observable_state state = observable_state::idle;
size_t demand = 0;
observer<T> out;
};
/// @relates passive_observable
template <class T>
intrusive_ptr<passive_observable<T>> make_passive_observable(coordinator* ctx) {
return make_counted<passive_observable<T>>(ctx);
}
/// Implementation of the `defer` operator.
template <class T, class Factory>
class defer_observable_impl : public ref_counted, public observable_impl<T> {
public:
// -- constructors, destructors, and assignment operators --------------------
defer_observable_impl(coordinator* ctx, Factory factory)
: ctx_(ctx), factory_(std::move(factory)) {
// nop
}
// -- implementation of disposable_impl --------------------------------------
void ref_disposable() const noexcept final {
this->ref();
}
void deref_disposable() const noexcept final {
this->deref();
}
void dispose() override {
factory_ = std::nullopt;
}
bool disposed() const noexcept override {
return factory_.has_value();
}
// -- implementation of observable<T>::impl ----------------------------------
coordinator* ctx() const noexcept final {
return ctx_;
}
disposable subscribe(observer<T> what) override {
if (factory_) {
return (*factory_)().subscribe(what);
} else {
what.on_error(make_error(sec::invalid_observable));
return disposable{};
}
}
private:
coordinator* ctx_;
std::optional<Factory> factory_;
};
} // namespace caf::flow } // namespace caf::flow
...@@ -7,10 +7,20 @@ ...@@ -7,10 +7,20 @@
#include "caf/async/spsc_buffer.hpp" #include "caf/async/spsc_buffer.hpp"
#include "caf/defaults.hpp" #include "caf/defaults.hpp"
#include "caf/detail/core_export.hpp" #include "caf/detail/core_export.hpp"
#include "caf/detail/ref_counted_base.hpp"
#include "caf/flow/coordinator.hpp" #include "caf/flow/coordinator.hpp"
#include "caf/flow/observable.hpp" #include "caf/flow/observable.hpp"
#include "caf/flow/op/defer.hpp"
#include "caf/flow/op/empty.hpp"
#include "caf/flow/op/fail.hpp"
#include "caf/flow/op/from_generator.hpp"
#include "caf/flow/op/from_resource.hpp"
#include "caf/flow/op/interval.hpp"
#include "caf/flow/op/never.hpp"
#include "caf/flow/op/zip_with.hpp"
#include <cstdint> #include <cstdint>
#include <functional>
namespace caf::flow { namespace caf::flow {
...@@ -30,60 +40,10 @@ class callable_source; ...@@ -30,60 +40,10 @@ class callable_source;
// -- special-purpose observable implementations ------------------------------- // -- special-purpose observable implementations -------------------------------
class interval_action;
class CAF_CORE_EXPORT interval_impl : public observable_impl_base<int64_t> {
public:
// -- member types -----------------------------------------------------------
using output_type = int64_t;
using super = observable_impl_base<int64_t>;
// -- friends ----------------------------------------------------------------
CAF_INTRUSIVE_PTR_FRIENDS(interval_impl)
friend class interval_action;
// -- constructors, destructors, and assignment operators --------------------
interval_impl(coordinator* ctx, timespan initial_delay, timespan period);
interval_impl(coordinator* ctx, timespan initial_delay, timespan period,
int64_t max_val);
// -- implementation of disposable::impl -------------------------------------
void dispose() override;
bool disposed() const noexcept override;
// -- implementation of observable<T>::impl ----------------------------------
void on_request(disposable_impl*, size_t) override;
void on_cancel(disposable_impl*) override;
disposable subscribe(observer<int64_t> what) override;
private:
void fire(interval_action*);
observer<int64_t> obs_;
disposable pending_;
timespan initial_delay_;
timespan period_;
coordinator::steady_time_point last_;
int64_t val_ = 0;
int64_t max_;
size_t demand_ = 0;
};
// -- builder interface -------------------------------------------------------- // -- builder interface --------------------------------------------------------
/// Factory for @ref observable objects. /// Factory for @ref observable objects.
class observable_builder { class CAF_CORE_EXPORT observable_builder {
public: public:
friend class coordinator; friend class coordinator;
...@@ -114,6 +74,13 @@ public: ...@@ -114,6 +74,13 @@ public:
return from_callable([x = std::move(init)]() mutable { return x++; }); return from_callable([x = std::move(init)]() mutable { return x++; });
} }
/// Creates a @ref generation that emits `num` ascending values, starting with
/// `init`.
template <class T>
[[nodiscard]] auto range(T init, size_t num) const {
return iota(init).take(num);
}
/// Creates a @ref generation that emits values by repeatedly calling /// Creates a @ref generation that emits values by repeatedly calling
/// `pullable.pull(...)`. For example implementations of the `Pullable` /// `pullable.pull(...)`. For example implementations of the `Pullable`
/// concept, see @ref container_source, @ref repeater_source and /// concept, see @ref container_source, @ref repeater_source and
...@@ -134,11 +101,7 @@ public: ...@@ -134,11 +101,7 @@ public:
[[nodiscard]] observable<int64_t> [[nodiscard]] observable<int64_t>
interval(std::chrono::duration<Rep, Period> initial_delay, interval(std::chrono::duration<Rep, Period> initial_delay,
std::chrono::duration<Rep, Period> period) { std::chrono::duration<Rep, Period> period) {
// Intervals introduce a time-dependency, so we need to watch them in order return make_observable<op::interval>(ctx_, initial_delay, period);
// to prevent actors from shutting down while timeouts are still pending.
auto ptr = make_counted<interval_impl>(ctx_, initial_delay, period);
ctx_->watch(ptr->as_disposable());
return observable<int64_t>{std::move(ptr)};
} }
/// Creates an @ref observable that emits a sequence of integers spaced by the /// Creates an @ref observable that emits a sequence of integers spaced by the
...@@ -154,39 +117,98 @@ public: ...@@ -154,39 +117,98 @@ public:
template <class Rep, class Period> template <class Rep, class Period>
[[nodiscard]] observable<int64_t> [[nodiscard]] observable<int64_t>
timer(std::chrono::duration<Rep, Period> delay) { timer(std::chrono::duration<Rep, Period> delay) {
auto ptr = make_counted<interval_impl>(ctx_, delay, delay, 1); return make_observable<op::interval>(ctx_, delay, delay, 1);
ctx_->watch(ptr->as_disposable());
return observable<int64_t>{std::move(ptr)};
} }
/// Creates an @ref observable without any values that calls `on_complete` /// Creates an @ref observable without any values that calls `on_complete`
/// after subscribing to it. /// after subscribing to it.
template <class T> template <class T>
[[nodiscard]] observable<T> empty() { [[nodiscard]] observable<T> empty() {
return make_observable<empty_observable_impl<T>>(ctx_); return make_observable<op::empty<T>>(ctx_);
} }
/// Creates an @ref observable without any values that also never terminates. /// Creates an @ref observable without any values that also never terminates.
template <class T> template <class T>
[[nodiscard]] observable<T> never() { [[nodiscard]] observable<T> never() {
return make_observable<mute_observable_impl<T>>(ctx_); return make_observable<op::never<T>>(ctx_);
} }
/// Creates an @ref observable without any values that calls `on_error` after /// Creates an @ref observable without any values that fails immediately when
/// subscribing to it. /// subscribing to it by calling `on_error` on the subscriber.
template <class T> template <class T>
[[nodiscard]] observable<T> error(caf::error what) { [[nodiscard]] observable<T> fail(error what) {
return make_observable<observable_error_impl<T>>(ctx_, std::move(what)); return make_observable<op::fail<T>>(ctx_, std::move(what));
} }
/// Create a fresh @ref observable for each @ref observer using the factory. /// Create a fresh @ref observable for each @ref observer using the factory.
template <class Factory> template <class Factory>
[[nodiscard]] auto defer(Factory factory) { [[nodiscard]] auto defer(Factory factory) {
using res_t = decltype(factory()); return make_observable<op::defer<Factory>>(ctx_, std::move(factory));
static_assert(is_observable_v<res_t>); }
using out_t = typename res_t::output_type;
using impl_t = defer_observable_impl<out_t, Factory>; /// Creates an @ref observable that combines the items emitted from all passed
return make_observable<impl_t>(ctx_, std::move(factory)); /// source observables.
template <class Input, class... Inputs>
auto merge(Input&& x, Inputs&&... xs) {
using in_t = std::decay_t<Input>;
if constexpr (is_observable_v<in_t>) {
using impl_t = op::merge<output_type_t<in_t>>;
return make_observable<impl_t>(ctx_, std::forward<Input>(x),
std::forward<Inputs>(xs)...);
} else {
static_assert(detail::is_iterable_v<in_t>);
using val_t = typename in_t::value_type;
static_assert(is_observable_v<val_t>);
using impl_t = op::merge<output_type_t<val_t>>;
return make_observable<impl_t>(ctx_, std::forward<Input>(x),
std::forward<Inputs>(xs)...);
}
}
/// Creates an @ref observable that concatenates the items emitted from all
/// passed source observables.
template <class Input, class... Inputs>
auto concat(Input&& x, Inputs&&... xs) {
using in_t = std::decay_t<Input>;
if constexpr (is_observable_v<in_t>) {
using impl_t = op::concat<output_type_t<in_t>>;
return make_observable<impl_t>(ctx_, std::forward<Input>(x),
std::forward<Inputs>(xs)...);
} else {
static_assert(detail::is_iterable_v<in_t>);
using val_t = typename in_t::value_type;
static_assert(is_observable_v<val_t>);
using impl_t = op::concat<output_type_t<val_t>>;
return make_observable<impl_t>(ctx_, std::forward<Input>(x),
std::forward<Inputs>(xs)...);
}
}
/// @param fn The zip function. Takes one element from each input at a time
/// and reduces them into a single result.
/// @param input0 The input at index 0.
/// @param input1 The input at index 1.
/// @param inputs The inputs for index > 1, if any.
template <class F, class T0, class T1, class... Ts>
auto zip_with(F fn, T0 input0, T1 input1, Ts... inputs) {
using output_type = op::zip_with_output_t<F, //
typename T0::output_type,
typename T1::output_type,
typename Ts::output_type...>;
using impl_t = op::zip_with<F, //
typename T0::output_type, //
typename T1::output_type, //
typename Ts::output_type...>;
if (input0.valid() && input1.valid() && (inputs.valid() && ...)) {
auto ptr = make_counted<impl_t>(ctx_, std::move(fn),
std::move(input0).as_observable(),
std::move(input1).as_observable(),
std::move(inputs).as_observable()...);
return observable<output_type>{std::move(ptr)};
} else {
auto ptr = make_counted<op::empty<output_type>>(ctx_);
return observable<output_type>{std::move(ptr)};
}
} }
private: private:
...@@ -209,12 +231,20 @@ public: ...@@ -209,12 +231,20 @@ public:
pos_ = values_.begin(); pos_ = values_.begin();
} }
container_source() = default;
container_source(container_source&&) = default; container_source(container_source&&) = default;
container_source& operator=(container_source&&) = default; container_source& operator=(container_source&&) = default;
container_source() = delete; container_source(const container_source& other) : values_(other.values_) {
container_source(const container_source&) = delete; pos_ = values_.begin();
container_source& operator=(const container_source&) = delete; std::advance(pos_, std::distance(other.values_.begin(), other.pos_));
}
container_source& operator=(const container_source& other) {
values_ = other.values_;
pos_ = values_.begin();
std::advance(pos_, std::distance(other.values_.begin(), other.pos_));
return *this;
}
template <class Step, class... Steps> template <class Step, class... Steps>
void pull(size_t n, Step& step, Steps&... steps) { void pull(size_t n, Step& step, Steps&... steps) {
...@@ -303,10 +333,9 @@ public: ...@@ -303,10 +333,9 @@ public:
} }
callable_source(callable_source&&) = default; callable_source(callable_source&&) = default;
callable_source(const callable_source&) = default;
callable_source& operator=(callable_source&&) = default; callable_source& operator=(callable_source&&) = default;
callable_source& operator=(const callable_source&) = default;
callable_source(const callable_source&) = delete;
callable_source& operator=(const callable_source&) = delete;
template <class Step, class... Steps> template <class Step, class... Steps>
void pull(size_t n, Step& step, Steps&... steps) { void pull(size_t n, Step& step, Steps&... steps) {
...@@ -338,61 +367,6 @@ class generation final ...@@ -338,61 +367,6 @@ class generation final
public: public:
using output_type = steps_output_type_t<Generator, Steps...>; using output_type = steps_output_type_t<Generator, Steps...>;
class impl : public observable_impl_base<output_type> {
public:
using super = observable_impl_base<output_type>;
template <class... Ts>
impl(coordinator* ctx, Generator gen, Ts&&... steps)
: super(ctx), gen_(std::move(gen)), steps_(std::forward<Ts>(steps)...) {
// nop
}
// -- implementation of disposable::impl -----------------------------------
void dispose() override {
term_.dispose();
}
bool disposed() const noexcept override {
return !term_.active();
}
// -- implementation of observable_impl<T> ---------------------------------
disposable subscribe(observer<output_type> sink) override {
CAF_LOG_TRACE(CAF_ARG2("sink", sink.ptr()));
auto sub = term_.add(this, sink);
if (sub) {
term_.start();
}
return sub;
}
void on_request(disposable_impl* sink, size_t demand) override {
CAF_LOG_TRACE(CAF_ARG(sink) << CAF_ARG(demand));
if (auto n = term_.on_request(sink, demand); n > 0) {
auto fn = [this, n](auto&... steps) { gen_.pull(n, steps..., term_); };
std::apply(fn, steps_);
term_.push();
}
}
void on_cancel(disposable_impl* sink) override {
CAF_LOG_TRACE(CAF_ARG(sink));
if (auto n = term_.on_cancel(sink); n > 0) {
auto fn = [this, n](auto&... steps) { gen_.pull(n, steps..., term_); };
std::apply(fn, steps_);
term_.push();
}
}
private:
Generator gen_;
std::tuple<Steps...> steps_;
broadcast_step<output_type> term_;
};
template <class... Ts> template <class... Ts>
generation(coordinator* ctx, Generator gen, Ts&&... steps) generation(coordinator* ctx, Generator gen, Ts&&... steps)
: ctx_(ctx), gen_(std::move(gen)), steps_(std::forward<Ts>(steps)...) { : ctx_(ctx), gen_(std::move(gen)), steps_(std::forward<Ts>(steps)...) {
...@@ -426,20 +400,73 @@ public: ...@@ -426,20 +400,73 @@ public:
filter_step<Predicate>{std::move(predicate)}); filter_step<Predicate>{std::move(predicate)});
} }
template <class Predicate>
auto take_while(Predicate predicate) && {
return std::move(*this).transform(
take_while_step<Predicate>{std::move(predicate)});
}
template <class Reducer>
auto reduce(output_type init, Reducer reducer) && {
return std::move(*this).transform(
reduce_step<output_type, Reducer>{init, reducer});
}
auto sum() && {
return std::move(*this).reduce(output_type{}, std::plus<output_type>{});
}
auto distinct() && {
return std::move(*this).transform(distinct_step<output_type>{});
}
template <class Fn> template <class Fn>
auto map(Fn fn) && { auto map(Fn fn) && {
return std::move(*this).transform(map_step<Fn>{std::move(fn)}); return std::move(*this).transform(map_step<Fn>{std::move(fn)});
} }
template <class... Inputs>
auto merge(Inputs&&... xs) && {
return std::move(*this).as_observable().merge(std::forward<Inputs>(xs)...);
}
template <class... Inputs>
auto concat(Inputs&&... xs) && {
return std::move(*this).as_observable().concat(std::forward<Inputs>(xs)...);
}
template <class F> template <class F>
auto flat_map_optional(F f) && { auto flat_map_optional(F f) && {
return std::move(*this).transform(flat_map_optional_step<F>{std::move(f)}); return std::move(*this).transform(flat_map_optional_step<F>{std::move(f)});
} }
template <class F>
auto do_on_next(F f) {
return std::move(*this) //
.transform(do_on_next_step<output_type, F>{std::move(f)});
}
template <class F>
auto do_on_complete(F f) && {
return std::move(*this).transform(
do_on_complete_step<output_type, F>{std::move(f)});
}
template <class F>
auto do_on_error(F f) && {
return std::move(*this).transform(
do_on_error_step<output_type, F>{std::move(f)});
}
template <class F>
auto do_finally(F f) && {
return std::move(*this).transform(
do_finally_step<output_type, F>{std::move(f)});
}
observable<output_type> as_observable() && override { observable<output_type> as_observable() && override {
auto pimpl = make_observable_impl<impl>(ctx_, std::move(gen_), using impl_t = op::from_generator<Generator, Steps...>;
std::move(steps_)); return make_observable<impl_t>(ctx_, std::move(gen_), std::move(steps_));
return observable<output_type>{std::move(pimpl)};
} }
coordinator* ctx() const noexcept { coordinator* ctx() const noexcept {
...@@ -490,18 +517,8 @@ generation<callable_source<F>> observable_builder::from_callable(F fn) const { ...@@ -490,18 +517,8 @@ generation<callable_source<F>> observable_builder::from_callable(F fn) const {
template <class T> template <class T>
observable<T> observable<T>
observable_builder::from_resource(async::consumer_resource<T> hdl) const { observable_builder::from_resource(async::consumer_resource<T> hdl) const {
using buffer_type = typename async::consumer_resource<T>::buffer_type; using impl_t = op::from_resource<T>;
using res_t = observable<T>; return make_observable<impl_t>(ctx_, std::move(hdl));
if (auto buf = hdl.try_open()) {
auto adapter = make_counted<observable_buffer_impl<buffer_type>>(ctx_, buf);
buf->set_consumer(adapter);
ctx_->watch(adapter->as_disposable());
return res_t{std::move(adapter)};
} else {
auto err = make_error(sec::invalid_observable,
"from_resource: failed to open the resource");
return make_observable<observable_error_impl<T>>(ctx_, std::move(err));
}
} }
// -- observable_builder::lift ------------------------------------------------- // -- observable_builder::lift -------------------------------------------------
......
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/disposable.hpp"
#include "caf/flow/fwd.hpp"
#include "caf/flow/op/base.hpp"
#include "caf/flow/step.hpp"
#include "caf/fwd.hpp"
#include "caf/intrusive_ptr.hpp"
#include <cstddef>
#include <functional>
#include <utility>
#include <vector>
namespace caf::flow {
/// Represents a potentially unbound sequence of values.
template <class T>
class observable {
public:
/// The type of emitted items.
using output_type = T;
/// The pointer-to-implementation type.
using pimpl_type = intrusive_ptr<op::base<T>>;
explicit observable(pimpl_type pimpl) noexcept : pimpl_(std::move(pimpl)) {
// nop
}
observable& operator=(std::nullptr_t) noexcept {
pimpl_.reset();
return *this;
}
observable() noexcept = default;
observable(observable&&) noexcept = default;
observable(const observable&) noexcept = default;
observable& operator=(observable&&) noexcept = default;
observable& operator=(const observable&) noexcept = default;
/// @copydoc impl::subscribe
disposable subscribe(observer<T> what) {
if (pimpl_) {
return pimpl_->subscribe(std::move(what));
} else {
what.on_error(make_error(sec::invalid_observable));
return disposable{};
}
}
/// Creates a new observer that pushes all observed items to the resource.
disposable subscribe(async::producer_resource<T> resource);
/// Returns a transformation that applies a step function to each input.
template <class Step>
transformation<Step> transform(Step step);
/// Registers a callback for `on_next` events.
template <class F>
auto do_on_next(F f) {
return transform(do_on_next_step<T, F>{std::move(f)});
}
/// Registers a callback for `on_complete` events.
template <class F>
auto do_on_complete(F f) {
return transform(do_on_complete_step<T, F>{std::move(f)});
}
/// Registers a callback for `on_error` events.
template <class F>
auto do_on_error(F f) {
return transform(do_on_error_step<T, F>{std::move(f)});
}
/// Registers a callback that runs on `on_complete` or `on_error`.
template <class F>
auto do_finally(F f) {
return transform(do_finally_step<T, F>{std::move(f)});
}
/// Catches errors by converting them into errors instead.
auto on_error_complete() {
return transform(on_error_complete_step<T>{});
}
/// Returns a transformation that selects only the first `n` items.
transformation<limit_step<T>> take(size_t n);
/// Returns a transformation that selects only items that satisfy `predicate`.
template <class Predicate>
transformation<filter_step<Predicate>> filter(Predicate prediate);
/// Returns a transformation that selects all value until the `predicate`
/// returns false.
template <class Predicate>
transformation<take_while_step<Predicate>> take_while(Predicate prediate);
/// Reduces the entire sequence of items to a single value. Other names for
/// the algorithm are `accumulate` and `fold`.
template <class Reducer>
transformation<reduce_step<T, Reducer>> reduce(T init, Reducer reducer);
/// Accumulates all values and emits only the final result.
auto sum() {
return reduce(T{}, std::plus<T>{});
}
/// Makes all values unique by suppressing all items that have been emitted in
/// the past.
transformation<distinct_step<T>> distinct();
/// Returns a transformation that applies `f` to each input and emits the
/// result of the function application.
template <class F>
transformation<map_step<F>> map(F f);
/// Calls `on_next` for each item emitted by this observable.
template <class OnNext>
disposable for_each(OnNext on_next);
/// Calls `on_next` for each item and `on_error` for each error emitted by
/// this observable.
template <class OnNext, class OnError>
disposable for_each(OnNext on_next, OnError on_error);
/// Calls `on_next` for each item, `on_error` for each error and `on_complete`
/// for each completion signal emitted by this observable.
template <class OnNext, class OnError, class OnComplete>
disposable for_each(OnNext on_next, OnError on_error, OnComplete on_complete);
/// Combines the output of multiple @ref observable objects into one by
/// merging their outputs. May also be called without arguments if the `T` is
/// an @ref observable.
template <class... Inputs>
auto merge(Inputs&&... xs);
/// Combines the output of multiple @ref observable objects into one by
/// concatenating their outputs. May also be called without arguments if the
/// `T` is an @ref observable.
template <class... Inputs>
auto concat(Inputs&&...);
/// Returns a transformation that emits items by merging the outputs of all
/// observables returned by `f`.
template <class F>
auto flat_map(F f);
/// Returns a transformation that emits items from optional values returned by
/// `f`.
template <class F>
transformation<flat_map_optional_step<F>> flat_map_optional(F f);
/// Returns a transformation that emits items by concatenating the outputs of
/// all observables returned by `f`.
template <class F>
auto concat_map(F f);
/// Takes @p prefix_size elements from this observable and emits it in a tuple
/// containing an observable for the remaining elements as the second value.
/// The returned observable either emits a single element (the tuple) or none
/// if this observable never produces sufficient elements for the prefix.
/// @pre `prefix_size > 0`
observable<cow_tuple<std::vector<T>, observable<T>>>
prefix_and_tail(size_t prefix_size);
/// Similar to `prefix_and_tail(1)` but passes the single element directly in
/// the tuple instead of wrapping it in a list.
observable<cow_tuple<T, observable<T>>> head_and_tail();
/// Creates an asynchronous resource that makes emitted items available in a
/// spsc buffer.
async::consumer_resource<T> to_resource(size_t buffer_size,
size_t min_request_size);
async::consumer_resource<T> to_resource() {
return to_resource(defaults::flow::buffer_size, defaults::flow::min_demand);
}
observable observe_on(coordinator* other, size_t buffer_size,
size_t min_request_size);
observable observe_on(coordinator* other) {
return observe_on(other, defaults::flow::buffer_size,
defaults::flow::min_demand);
}
const observable& as_observable() const& noexcept {
return std::move(*this);
}
observable&& as_observable() && noexcept {
return std::move(*this);
}
const pimpl_type& pimpl() const noexcept {
return pimpl_;
}
bool valid() const noexcept {
return pimpl_ != nullptr;
}
explicit operator bool() const noexcept {
return valid();
}
bool operator!() const noexcept {
return !valid();
}
void swap(observable& other) {
pimpl_.swap(other.pimpl_);
}
/// @pre `valid()`
coordinator* ctx() const {
return pimpl_->ctx();
}
private:
pimpl_type pimpl_;
};
/// Convenience function for creating an @ref observable from a concrete
/// operator type.
/// @relates observable
template <class Operator, class... Ts>
observable<typename Operator::output_type>
make_observable(coordinator* ctx, Ts&&... xs) {
using out_t = typename Operator::output_type;
using ptr_t = intrusive_ptr<op::base<out_t>>;
ptr_t ptr{new Operator(ctx, std::forward<Ts>(xs)...), false};
return observable<out_t>{std::move(ptr)};
}
// Note: the definition of all member functions is in observable.hpp.
} // namespace caf::flow
...@@ -7,9 +7,11 @@ ...@@ -7,9 +7,11 @@
#include "caf/async/batch.hpp" #include "caf/async/batch.hpp"
#include "caf/async/producer.hpp" #include "caf/async/producer.hpp"
#include "caf/defaults.hpp" #include "caf/defaults.hpp"
#include "caf/detail/comparable.hpp"
#include "caf/detail/type_traits.hpp" #include "caf/detail/type_traits.hpp"
#include "caf/disposable.hpp" #include "caf/disposable.hpp"
#include "caf/error.hpp" #include "caf/error.hpp"
#include "caf/flow/coordinated.hpp"
#include "caf/flow/coordinator.hpp" #include "caf/flow/coordinator.hpp"
#include "caf/flow/observer_state.hpp" #include "caf/flow/observer_state.hpp"
#include "caf/flow/subscription.hpp" #include "caf/flow/subscription.hpp"
...@@ -23,10 +25,10 @@ namespace caf::flow { ...@@ -23,10 +25,10 @@ namespace caf::flow {
/// Handle to a consumer of items. /// Handle to a consumer of items.
template <class T> template <class T>
class observer { class observer : detail::comparable<observer<T>> {
public: public:
/// Internal interface of an `observer`. /// Internal interface of an `observer`.
class impl : public disposable::impl { class impl : public coordinated {
public: public:
using input_type = T; using input_type = T;
...@@ -61,14 +63,6 @@ public: ...@@ -61,14 +63,6 @@ public:
observer& operator=(observer&&) noexcept = default; observer& operator=(observer&&) noexcept = default;
observer& operator=(const observer&) noexcept = default; observer& operator=(const observer&) noexcept = default;
disposable as_disposable() const& noexcept {
return disposable{pimpl_};
}
disposable as_disposable() && noexcept {
return disposable{std::move(pimpl_)};
}
/// @pre `valid()` /// @pre `valid()`
void on_complete() { void on_complete() {
pimpl_->on_complete(); pimpl_->on_complete();
...@@ -129,10 +123,14 @@ public: ...@@ -129,10 +123,14 @@ public:
return std::move(pimpl_); return std::move(pimpl_);
} }
void swap(observer& other) { void swap(observer& other) noexcept {
pimpl_.swap(other.pimpl_); pimpl_.swap(other.pimpl_);
} }
intptr_t compare(const observer& other) const noexcept {
return pimpl_.compare(other.pimpl_);
}
private: private:
intrusive_ptr<impl> pimpl_; intrusive_ptr<impl> pimpl_;
}; };
...@@ -166,7 +164,7 @@ using on_next_value_type = typename on_next_trait_t<F>::value_type; ...@@ -166,7 +164,7 @@ using on_next_value_type = typename on_next_trait_t<F>::value_type;
template <class OnNext, class OnError = unit_t, class OnComplete = unit_t> template <class OnNext, class OnError = unit_t, class OnComplete = unit_t>
class default_observer_impl class default_observer_impl
: public ref_counted, : public detail::ref_counted_base,
public flow::observer_impl<on_next_value_type<OnNext>> { public flow::observer_impl<on_next_value_type<OnNext>> {
public: public:
static_assert(std::is_invocable_v<OnError, const error&>); static_assert(std::is_invocable_v<OnError, const error&>);
...@@ -175,8 +173,6 @@ public: ...@@ -175,8 +173,6 @@ public:
using input_type = on_next_value_type<OnNext>; using input_type = on_next_value_type<OnNext>;
CAF_INTRUSIVE_PTR_FRIENDS(default_observer_impl)
explicit default_observer_impl(OnNext&& on_next_fn) explicit default_observer_impl(OnNext&& on_next_fn)
: on_next_(std::move(on_next_fn)) { : on_next_(std::move(on_next_fn)) {
// nop // nop
...@@ -195,39 +191,23 @@ public: ...@@ -195,39 +191,23 @@ public:
// nop // nop
} }
void ref_disposable() const noexcept final {
this->ref();
}
void deref_disposable() const noexcept final {
this->deref();
}
void on_next(const input_type& item) override { void on_next(const input_type& item) override {
if (!completed_) {
on_next_(item); on_next_(item);
sub_.request(1); sub_.request(1);
} }
}
void on_error(const error& what) override { void on_error(const error& what) override {
if (!completed_) {
on_error_(what); on_error_(what);
sub_ = nullptr; sub_ = nullptr;
completed_ = true;
}
} }
void on_complete() override { void on_complete() override {
if (!completed_) {
on_complete_(); on_complete_();
sub_ = nullptr; sub_ = nullptr;
completed_ = true;
}
} }
void on_subscribe(flow::subscription sub) override { void on_subscribe(flow::subscription sub) override {
if (!completed_ && !sub_) { if (!sub_) {
sub_ = std::move(sub); sub_ = std::move(sub);
sub_.request(defaults::flow::buffer_size); sub_.request(defaults::flow::buffer_size);
} else { } else {
...@@ -235,23 +215,15 @@ public: ...@@ -235,23 +215,15 @@ public:
} }
} }
void dispose() override { void ref_coordinated() const noexcept override {
if (!completed_) { this->ref();
on_complete_();
if (sub_) {
sub_.cancel();
sub_ = nullptr;
}
completed_ = true;
}
} }
bool disposed() const noexcept override { void deref_coordinated() const noexcept override {
return completed_; this->deref();
} }
private: private:
bool completed_ = false;
OnNext on_next_; OnNext on_next_;
OnError on_error_; OnError on_error_;
OnComplete on_complete_; OnComplete on_complete_;
...@@ -313,7 +285,7 @@ auto make_observer_from_ptr(SmartPointer ptr) { ...@@ -313,7 +285,7 @@ auto make_observer_from_ptr(SmartPointer ptr) {
/// Writes observed values to a bounded buffer. /// Writes observed values to a bounded buffer.
template <class Buffer> template <class Buffer>
class buffer_writer_impl : public ref_counted, class buffer_writer_impl : public detail::ref_counted_base,
public observer_impl<typename Buffer::value_type>, public observer_impl<typename Buffer::value_type>,
public async::producer { public async::producer {
public: public:
...@@ -340,22 +312,13 @@ public: ...@@ -340,22 +312,13 @@ public:
buf_->close(); buf_->close();
} }
// -- implementation of disposable::impl ------------------------------------- // -- implementation of coordinated ------------------------------------------
void dispose() override {
CAF_LOG_TRACE("");
on_complete();
}
bool disposed() const noexcept override {
return buf_ == nullptr;
}
void ref_disposable() const noexcept final { void ref_coordinated() const noexcept final {
this->ref(); this->ref();
} }
void deref_disposable() const noexcept final { void deref_coordinated() const noexcept final {
this->deref(); this->deref();
} }
...@@ -455,116 +418,172 @@ private: ...@@ -455,116 +418,172 @@ private:
/// Forwards all events to a parent. /// Forwards all events to a parent.
template <class T, class Parent, class Token> template <class T, class Parent, class Token>
class forwarder : public ref_counted, public observer_impl<T> { class forwarder : public detail::ref_counted_base, public observer_impl<T> {
public: public:
CAF_INTRUSIVE_PTR_FRIENDS(forwarder) // -- constructors, destructors, and assignment operators --------------------
explicit forwarder(intrusive_ptr<Parent> parent, Token token) explicit forwarder(intrusive_ptr<Parent> parent, Token token)
: parent(std::move(parent)), token(std::move(token)) { : parent_(std::move(parent)), token_(std::move(token)) {
// nop // nop
} }
// -- implementation of coordinated ------------------------------------------
void ref_coordinated() const noexcept final {
this->ref();
}
void deref_coordinated() const noexcept final {
this->deref();
}
// -- implementation of observer_impl<T> -------------------------------------
void on_complete() override { void on_complete() override {
if (parent) { if (parent_) {
parent->fwd_on_complete(token); parent_->fwd_on_complete(token_);
parent = nullptr; parent_ = nullptr;
} }
} }
void on_error(const error& what) override { void on_error(const error& what) override {
if (parent) { if (parent_) {
parent->fwd_on_error(token, what); parent_->fwd_on_error(token_, what);
parent = nullptr; parent_ = nullptr;
} }
} }
void on_subscribe(subscription new_sub) override { void on_subscribe(subscription new_sub) override {
if (parent) { if (parent_) {
parent->fwd_on_subscribe(token, std::move(new_sub)); parent_->fwd_on_subscribe(token_, std::move(new_sub));
} else { } else {
new_sub.cancel(); new_sub.cancel();
} }
} }
void on_next(const T& item) override { void on_next(const T& item) override {
if (parent) { if (parent_) {
parent->fwd_on_next(token, item); parent_->fwd_on_next(token_, item);
} }
} }
void dispose() override { private:
on_complete(); intrusive_ptr<Parent> parent_;
} Token token_;
};
bool disposed() const noexcept override { /// An observer with minimal internal logic. Useful for writing unit tests.
return !parent; template <class T>
} class passive_observer : public detail::ref_counted_base,
public observer_impl<T> {
public:
// -- implementation of coordinated ------------------------------------------
void ref_disposable() const noexcept final { void ref_coordinated() const noexcept final {
this->ref(); this->ref();
} }
void deref_disposable() const noexcept final { void deref_coordinated() const noexcept final {
this->deref(); this->deref();
} }
intrusive_ptr<Parent> parent; // -- implementation of observer_impl<T> -------------------------------------
Token token;
};
/// An observer with minimal internal logic. Useful for writing unit tests.
template <class T>
class passive_observer : public ref_counted, public observer_impl<T> {
public:
// -- friends ----------------------------------------------------------------
CAF_INTRUSIVE_PTR_FRIENDS(passive_observer)
// -- implementation of disposable::impl ------------------------------------- void on_complete() override {
if (sub) {
sub.cancel();
sub = nullptr;
}
state = observer_state::completed;
}
void dispose() override { void on_error(const error& what) override {
if (!disposed()) {
if (sub) { if (sub) {
sub.cancel(); sub.cancel();
sub = nullptr; sub = nullptr;
} }
state = observer_state::disposed; err = what;
state = observer_state::aborted;
}
void on_subscribe(subscription new_sub) override {
if (state == observer_state::idle) {
CAF_ASSERT(!sub);
sub = std::move(new_sub);
state = observer_state::subscribed;
} else {
new_sub.cancel();
} }
} }
bool disposed() const noexcept override { void on_next(const T& item) override {
switch (state) { buf.emplace_back(item);
case observer_state::completed: }
case observer_state::aborted:
case observer_state::disposed: // -- convenience functions --------------------------------------------------
bool request(size_t demand) {
if (sub) {
sub.request(demand);
return true; return true;
default: } else {
return false; return false;
} }
} }
void ref_disposable() const noexcept final { std::vector<T> sorted_buf() const {
auto result = buf;
std::sort(result.begin(), result.end());
return result;
}
// -- member variables -------------------------------------------------------
/// The subscription for requesting additional items.
subscription sub;
/// Default-constructed unless on_error was called.
error err;
/// Represents the current state of this observer.
observer_state state;
/// Stores all items received via `on_next`.
std::vector<T> buf;
};
/// @relates passive_observer
template <class T>
intrusive_ptr<passive_observer<T>> make_passive_observer() {
return make_counted<passive_observer<T>>();
}
/// Similar to @ref passive_observer but automatically requests items until
/// completed. Useful for writing unit tests.
template <class T>
class auto_observer : public detail::ref_counted_base, public observer_impl<T> {
public:
// -- implementation of coordinated ------------------------------------------
void ref_coordinated() const noexcept final {
this->ref(); this->ref();
} }
void deref_disposable() const noexcept final { void deref_coordinated() const noexcept final {
this->deref(); this->deref();
} }
// -- implementation of observer_impl<T> ------------------------------------- // -- implementation of observer_impl<T> -------------------------------------
void on_complete() override { void on_complete() override {
if (!disposed()) {
if (sub) { if (sub) {
sub.cancel(); sub.cancel();
sub = nullptr; sub = nullptr;
} }
state = observer_state::completed; state = observer_state::completed;
} }
}
void on_error(const error& what) override { void on_error(const error& what) override {
if (!disposed()) {
if (sub) { if (sub) {
sub.cancel(); sub.cancel();
sub = nullptr; sub = nullptr;
...@@ -572,13 +591,13 @@ public: ...@@ -572,13 +591,13 @@ public:
err = what; err = what;
state = observer_state::aborted; state = observer_state::aborted;
} }
}
void on_subscribe(subscription new_sub) override { void on_subscribe(subscription new_sub) override {
if (state == observer_state::idle) { if (state == observer_state::idle) {
CAF_ASSERT(!sub); CAF_ASSERT(!sub);
sub = std::move(new_sub); sub = std::move(new_sub);
state = observer_state::subscribed; state = observer_state::subscribed;
sub.request(64);
} else { } else {
new_sub.cancel(); new_sub.cancel();
} }
...@@ -586,6 +605,15 @@ public: ...@@ -586,6 +605,15 @@ public:
void on_next(const T& item) override { void on_next(const T& item) override {
buf.emplace_back(item); buf.emplace_back(item);
sub.request(1);
}
// -- convenience functions --------------------------------------------------
std::vector<T> sorted_buf() const {
auto result = buf;
std::sort(result.begin(), result.end());
return result;
} }
// -- member variables ------------------------------------------------------- // -- member variables -------------------------------------------------------
...@@ -603,10 +631,10 @@ public: ...@@ -603,10 +631,10 @@ public:
std::vector<T> buf; std::vector<T> buf;
}; };
/// @relates passive_observer /// @relates auto_observer
template <class T> template <class T>
intrusive_ptr<passive_observer<T>> make_passive_observer() { intrusive_ptr<auto_observer<T>> make_auto_observer() {
return make_counted<passive_observer<T>>(); return make_counted<auto_observer<T>>();
} }
} // namespace caf::flow } // namespace caf::flow
The namespace `caf::flow::op` contains (private) implementation classes for
various operators.
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/detail/ref_counted_base.hpp"
#include "caf/flow/observer.hpp"
#include "caf/flow/subscription.hpp"
namespace caf::flow::op {
/// Abstract base type for all flow operators.
template <class T>
class base : public coordinated {
public:
/// The type of observed values.
using output_type = T;
/// Returns the @ref coordinator that executes this flow operator.
virtual coordinator* ctx() const noexcept = 0;
/// Subscribes a new observer to the operator.
virtual disposable subscribe(observer<T> what) = 0;
};
} // namespace caf::flow::op
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/flow/observer.hpp"
#include "caf/flow/op/hot.hpp"
#include "caf/flow/subscription.hpp"
#include "caf/none.hpp"
#include <memory>
#include <utility>
#include <variant>
#include <vector>
namespace caf::flow::op {
/// State shared between one multicast operator and one subscribed observer.
template <class T>
struct cell_sub_state {
std::variant<none_t, unit_t, T, error> content;
std::vector<observer<T>> listeners;
void set_null() {
CAF_ASSERT(std::holds_alternative<none_t>(content));
content = unit;
std::vector<observer<T>> xs;
xs.swap(listeners);
for (auto& listener : xs) {
listener.on_complete();
}
}
void set_value(T item) {
CAF_ASSERT(std::holds_alternative<none_t>(content));
content = std::move(item);
auto& ref = std::get<T>(content);
std::vector<observer<T>> xs;
xs.swap(listeners);
for (auto& listener : xs) {
listener.on_next(ref);
listener.on_complete();
}
}
void set_error(error what) {
CAF_ASSERT(std::holds_alternative<none_t>(content));
content = std::move(what);
auto& ref = std::get<error>(content);
std::vector<observer<T>> xs;
xs.swap(listeners);
for (auto& listener : xs)
listener.on_error(ref);
}
void listen(observer<T> listener) {
switch (content.index()) {
case 1:
listener.on_complete();
break;
case 2:
listener.on_next(std::get<T>(content));
listener.on_complete();
break;
case 3:
listener.on_error(std::get<error>(content));
break;
default:
listeners.emplace_back(std::move(listener));
}
}
void drop(const observer<T>& listener) {
if (auto i = std::find(listeners.begin(), listeners.end(), listener);
i != listeners.end())
listeners.erase(i);
}
};
template <class T>
using cell_sub_state_ptr = std::shared_ptr<cell_sub_state<T>>;
template <class T>
class cell_sub : public subscription::impl_base {
public:
// -- constructors, destructors, and assignment operators --------------------
cell_sub(coordinator* ctx, cell_sub_state_ptr<T> state, observer<T> out)
: ctx_(ctx), state_(std::move(state)), out_(std::move(out)) {
// nop
}
// -- implementation of subscription -----------------------------------------
bool disposed() const noexcept override {
return !state_;
}
void cancel() override {
if (state_) {
state_->drop(out_);
state_ = nullptr;
out_ = nullptr;
}
}
void request(size_t) override {
if (!listening_) {
listening_ = true;
ctx_->delay_fn([state = state_, out = out_] { //
state->listen(std::move(out));
});
}
}
private:
coordinator* ctx_;
bool listening_ = false;
cell_sub_state_ptr<T> state_;
observer<T> out_;
};
// Base type for *hot* operators that multicast data to subscribed observers.
template <class T>
class cell : public hot<T> {
public:
// -- member types -----------------------------------------------------------
using super = hot<T>;
using state_type = cell_sub_state<T>;
using state_ptr_type = cell_sub_state_ptr<T>;
using observer_type = observer<T>;
// -- constructors, destructors, and assignment operators --------------------
explicit cell(coordinator* ctx)
: super(ctx), state_(std::make_shared<state_type>()) {
// nop
}
void set_null() {
state_->set_null();
}
void set_value(T item) {
state_->set_value(std::move(item));
}
void set_error(error what) {
state_->set_error(what);
}
disposable subscribe(observer<T> out) override {
auto ptr = make_counted<cell_sub<T>>(super::ctx_, state_, out);
out.on_subscribe(subscription{ptr});
return disposable{std::move(ptr)};
}
protected:
cell_sub_state_ptr<T> state_;
};
} // namespace caf::flow::op
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/detail/ref_counted_base.hpp"
#include "caf/flow/observer.hpp"
#include "caf/flow/op/base.hpp"
#include "caf/flow/subscription.hpp"
namespace caf::flow::op {
/// Convenience base type for *cold* observable types.
template <class T>
class cold : public detail::ref_counted_base, public base<T> {
public:
// -- member types -----------------------------------------------------------
using output_type = T;
// -- constructors, destructors, and assignment operators --------------------
explicit cold(coordinator* ctx) : ctx_(ctx) {
// nop
}
// -- implementation of disposable_impl --------------------------------------
void ref_coordinated() const noexcept final {
this->ref();
}
void deref_coordinated() const noexcept final {
this->deref();
}
// -- implementation of observable_impl<T> -----------------------------------
coordinator* ctx() const noexcept final {
return ctx_;
}
protected:
// -- member variables -------------------------------------------------------
coordinator* ctx_;
};
} // namespace caf::flow::op
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/detail/ref_counted_base.hpp"
#include "caf/flow/observer.hpp"
#include "caf/flow/op/cold.hpp"
#include "caf/flow/op/empty.hpp"
#include "caf/flow/subscription.hpp"
#include <deque>
#include <memory>
#include <numeric>
#include <utility>
#include <variant>
#include <vector>
namespace caf::flow::op {
/// Combines items from any number of observables.
template <class T>
class concat_sub : public subscription::impl_base {
public:
// -- member types -----------------------------------------------------------
using input_key = size_t;
using input_type = std::variant<observable<T>, observable<observable<T>>>;
// -- constructors, destructors, and assignment operators --------------------
concat_sub(coordinator* ctx, observer<T> out, std::vector<input_type> inputs)
: ctx_(ctx), out_(out), inputs_(std::move(inputs)) {
CAF_ASSERT(!inputs_.empty());
subscribe_next();
}
// -- input management -------------------------------------------------------
void subscribe_to(observable<T> what) {
CAF_ASSERT(!active_sub_);
active_key_ = next_key_++;
using fwd_t = forwarder<T, concat_sub, size_t>;
auto fwd = make_counted<fwd_t>(this, active_key_);
what.subscribe(fwd->as_observer());
}
void subscribe_to(observable<observable<T>> what) {
CAF_ASSERT(!active_sub_);
CAF_ASSERT(!factory_sub_);
factory_key_ = next_key_++;
using fwd_t = forwarder<observable<T>, concat_sub, size_t>;
auto fwd = make_counted<fwd_t>(this, factory_key_);
what.subscribe(fwd->as_observer());
}
void subscribe_next() {
if (factory_key_ != 0) {
// Ask the factory for the next observable.
CAF_ASSERT(!active_sub_);
factory_sub_.request(1);
} else if (!inputs_.empty()) {
// Subscribe to the next observable from the list.
std::visit([this](auto& x) { this->subscribe_to(x); }, inputs_.front());
inputs_.erase(inputs_.begin());
} else {
// Done!
fin();
}
}
// -- callbacks for the forwarders -------------------------------------------
void fwd_on_subscribe(input_key key, subscription sub) {
if (active_key_ == key && !active_sub_) {
active_sub_ = std::move(sub);
if (in_flight_ > 0)
active_sub_.request(in_flight_);
} else if (factory_key_ == key && !factory_sub_) {
CAF_ASSERT(!active_sub_);
factory_sub_ = std::move(sub);
factory_sub_.request(1);
} else {
sub.cancel();
}
}
void fwd_on_complete(input_key key) {
if (active_key_ == key && active_sub_) {
active_sub_ = nullptr;
subscribe_next();
} else if (factory_key_ == key && factory_sub_) {
factory_sub_ = nullptr;
factory_key_ = 0;
if (!active_sub_)
subscribe_next();
}
}
void fwd_on_error(input_key key, const error& what) {
if (key == active_key_ || key == factory_key_) {
CAF_ASSERT(out_);
if (delay_error_) {
if (!err_)
err_ = what;
subscribe_next();
} else {
err_ = what;
fin();
}
}
}
void fwd_on_next(input_key key, const T& item) {
if (key == active_key_) {
CAF_ASSERT(out_);
--in_flight_;
out_.on_next(item);
}
}
void fwd_on_next(input_key key, const observable<T>& item) {
if (key == factory_key_) {
CAF_ASSERT(!active_sub_);
subscribe_to(item);
}
}
// -- implementation of subscription -----------------------------------------
bool disposed() const noexcept override {
return !out_;
}
void cancel() override {
if (out_) {
ctx_->delay_fn([strong_this = intrusive_ptr<concat_sub>{this}] {
if (strong_this->out_) {
strong_this->err_.reset();
strong_this->fin();
}
});
}
}
void request(size_t n) override {
CAF_ASSERT(out_.valid());
if (active_sub_)
active_sub_.request(n);
in_flight_ += n;
}
private:
void fin() {
CAF_ASSERT(out_);
if (factory_sub_) {
factory_sub_.cancel();
factory_sub_ = nullptr;
}
if (active_sub_) {
active_sub_.cancel();
active_sub_ = nullptr;
}
factory_key_ = 0;
active_key_ = 0;
if (err_)
out_.on_error(err_);
else
out_.on_complete();
out_ = nullptr;
}
/// Stores the context (coordinator) that runs this flow.
coordinator* ctx_;
/// Stores a handle to the subscribed observer.
observer<T> out_;
/// Configures whether we carry on after an error.
bool delay_error_ = false;
/// Caches an on_error reason if delay_error_ is true.
error err_;
/// Stores our input sources. The first input is active (subscribed to) while
/// the others are pending (not subscribed to).
std::vector<input_type> inputs_;
/// If set, identifies the subscription to an observable factory.
subscription factory_sub_;
/// Our currently active subscription.
subscription active_sub_;
/// Identifies the active forwarder.
input_key factory_key_ = 0;
/// Identifies the active forwarder.
input_key active_key_ = 0;
/// Stores the next available key.
input_key next_key_ = 1;
/// Stores how much demand we have left. When switching to a new input, we
/// pass any demand unused by the previous input to the new one.
size_t in_flight_ = 0;
};
template <class T>
class concat : public cold<T> {
public:
// -- member types -----------------------------------------------------------
using super = cold<T>;
using input_type = std::variant<observable<T>, observable<observable<T>>>;
// -- constructors, destructors, and assignment operators --------------------
template <class... Ts, class... Inputs>
explicit concat(coordinator* ctx, Inputs&&... inputs) : super(ctx) {
(add(std::forward<Inputs>(inputs)), ...);
}
// -- properties -------------------------------------------------------------
size_t inputs() const noexcept {
return inputs_.size();
}
// -- implementation of observable<T> -----------------------------------
disposable subscribe(observer<T> out) override {
if (inputs() == 0) {
return make_counted<empty<T>>(super::ctx_)->subscribe(std::move(out));
} else {
auto ptr = make_counted<concat_sub<T>>(super::ctx_, out, inputs_);
out.on_subscribe(subscription{ptr});
return ptr->as_disposable();
}
}
private:
template <class Input>
void add(Input&& x) {
using input_t = std::decay_t<Input>;
if constexpr (detail::is_iterable_v<input_t>) {
for (auto& in : x)
add(in);
} else {
static_assert(is_observable_v<input_t>);
inputs_.emplace_back(std::move(x).as_observable());
}
}
std::vector<input_type> inputs_;
};
} // namespace caf::flow::op
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/flow/observer.hpp"
#include "caf/flow/op/cold.hpp"
#include "caf/flow/subscription.hpp"
#include <utility>
namespace caf::flow::op {
template <class Fn>
struct defer_trait {
using fn_result_type = decltype(std::declval<Fn&>()());
static_assert(is_observable_v<fn_result_type>);
using output_type = typename fn_result_type::output_type;
};
/// Implementation of the `defer` operator.
template <class Factory>
class defer : public cold<typename defer_trait<Factory>::output_type> {
public:
// -- member types -----------------------------------------------------------
using output_type = typename defer_trait<Factory>::output_type;
using super = cold<output_type>;
// -- constructors, destructors, and assignment operators --------------------
defer(coordinator* ctx, Factory fn) : super(ctx), fn_(std::move(fn)) {
// nop
}
// -- implementation of observable<T>::impl ----------------------------------
disposable subscribe(observer<output_type> what) override {
return fn_().subscribe(what);
}
private:
Factory fn_;
};
} // namespace caf::flow::op
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/flow/observable.hpp"
#include "caf/flow/observer.hpp"
#include "caf/flow/op/cold.hpp"
#include "caf/flow/subscription.hpp"
#include <cstdint>
namespace caf::flow::op {
template <class T>
class empty_sub : public subscription::impl_base {
public:
// -- constructors, destructors, and assignment operators --------------------
empty_sub(coordinator* ctx, observer<T> out)
: ctx_(ctx), out_(std::move(out)) {
// nop
}
// -- implementation of subscription -----------------------------------------
bool disposed() const noexcept override {
return !out_;
}
void cancel() override {
if (out_)
ctx_->delay_fn([out = std::move(out_)]() mutable { out.on_complete(); });
}
void request(size_t) override {
cancel();
}
private:
/// Stores the context (coordinator) that runs this flow.
coordinator* ctx_;
/// Stores a handle to the subscribed observer.
observer<T> out_;
};
/// An observable that represents an empty range. As soon as an observer
/// requests values from this observable, it calls `on_complete`.
template <class T>
class empty : public cold<T> {
public:
// -- member types -----------------------------------------------------------
using super = cold<T>;
using output_type = T;
// -- constructors, destructors, and assignment operators --------------------
explicit empty(coordinator* ctx) : super(ctx) {
// nop
}
// -- implementation of observable<T>::impl ----------------------------------
disposable subscribe(observer<output_type> out) override {
auto ptr = make_counted<empty_sub<T>>(super::ctx_, out);
out.on_subscribe(subscription{ptr});
return disposable{std::move(ptr)};
}
};
} // namespace caf::flow::op
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/error.hpp"
#include "caf/flow/observable.hpp"
#include "caf/flow/observer.hpp"
#include "caf/flow/op/cold.hpp"
#include "caf/flow/subscription.hpp"
namespace caf::flow::op {
template <class T>
class fail : public cold<T> {
public:
// -- member types -----------------------------------------------------------
using super = cold<T>;
// -- constructors, destructors, and assignment operators --------------------
fail(coordinator* ctx, error err) : super(ctx), err_(std::move(err)) {
// nop
}
// -- implementation of observable_impl<T> -----------------------------------
disposable subscribe(observer<T> out) override {
out.on_error(err_);
return {};
}
private:
error err_;
};
} // namespace caf::flow::op
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/detail/ref_counted_base.hpp"
#include "caf/detail/type_list.hpp"
#include "caf/flow/observer.hpp"
#include "caf/flow/op/hot.hpp"
#include "caf/flow/subscription.hpp"
#include "caf/sec.hpp"
#include <tuple>
#include <utility>
namespace caf::flow::op {
template <class Generator, class... Steps>
class from_generator_sub : public subscription::impl_base {
public:
// -- member types -----------------------------------------------------------
using output_type = steps_output_type_t<Generator, Steps...>;
// -- constructors, destructors, and assignment operators --------------------
from_generator_sub(coordinator* ctx, observer<output_type> out,
const Generator& gen, const std::tuple<Steps...>& steps)
: ctx_(ctx), out_(std::move(out)), gen_(gen), steps_(steps) {
// nop
}
// -- callbacks for the generator / steps ------------------------------------
bool on_next(const output_type& item) {
buf_.push_back(item);
return true;
}
void on_complete() {
completed_ = true;
}
void on_error(const error& what) {
completed_ = true;
err_ = what;
}
// -- implementation of subscription -----------------------------------------
bool disposed() const noexcept override {
return !out_;
}
void cancel() override {
if (out_) {
completed_ = true;
buf_.clear();
run_later();
}
}
void request(size_t n) override {
CAF_ASSERT(out_.valid());
demand_ += n;
run_later();
}
private:
void run_later() {
if (!running_) {
running_ = true;
ctx_->delay_fn([strong_this = intrusive_ptr<from_generator_sub>{this}] {
strong_this->do_run();
});
}
}
void do_run() {
while (out_ && demand_ > 0) {
while (buf_.empty() && !completed_)
pull(demand_);
if (!buf_.empty()) {
--demand_;
auto tmp = std::move(buf_.front());
buf_.pop_front();
out_.on_next(tmp);
} else if (completed_) {
fin();
running_ = false;
return;
}
}
if (out_ && buf_.empty() && completed_)
fin();
running_ = false;
}
void fin() {
if (!err_)
out_.on_complete();
else
out_.on_error(err_);
out_ = nullptr;
}
void pull(size_t n) {
auto fn = [this, n](auto&... steps) { gen_.pull(n, steps..., *this); };
std::apply(fn, steps_);
}
coordinator* ctx_;
bool running_ = false;
std::deque<output_type> buf_;
bool completed_ = false;
error err_;
size_t demand_ = 0;
observer<output_type> out_;
Generator gen_;
std::tuple<Steps...> steps_;
};
template <class Generator, class... Steps>
using from_generator_output_t = //
typename detail::tl_back_t< //
detail::type_list<Generator, Steps...> //
>::output_type;
template <class Generator, class... Steps>
class from_generator
: public op::cold<from_generator_output_t<Generator, Steps...>> {
public:
using output_type = from_generator_output_t<Generator, Steps...>;
using super = op::cold<output_type>;
from_generator(coordinator* ctx, Generator gen, std::tuple<Steps...> steps)
: super(ctx), gen_(std::move(gen)), steps_(std::move(steps)) {
// nop
}
// -- implementation of observable_impl<T> ---------------------------------
disposable subscribe(observer<output_type> out) override {
CAF_LOG_TRACE(CAF_ARG2("out", out.ptr()));
using impl_t = from_generator_sub<Generator, Steps...>;
auto ptr = make_counted<impl_t>(super::ctx_, out, gen_, steps_);
auto sub = subscription{std::move(ptr)};
out.on_subscribe(sub);
return std::move(sub).as_disposable();
}
private:
Generator gen_;
std::tuple<Steps...> steps_;
};
} // namespace caf::flow::op
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/detail/ref_counted_base.hpp"
#include "caf/detail/scope_guard.hpp"
#include "caf/flow/observer.hpp"
#include "caf/flow/op/hot.hpp"
#include "caf/flow/subscription.hpp"
#include "caf/sec.hpp"
#include <utility>
namespace caf::flow::op {
/// Reads from an observable buffer and emits the consumed items.
template <class Buffer>
class from_resource_sub : public subscription::impl_base,
public async::consumer {
public:
// -- intrusive_ptr interface ------------------------------------------------
friend void intrusive_ptr_add_ref(const from_resource_sub* ptr) noexcept {
ptr->ref();
}
friend void intrusive_ptr_release(const from_resource_sub* ptr) noexcept {
ptr->deref();
}
// -- member types -----------------------------------------------------------
using value_type = typename Buffer::value_type;
using buffer_ptr = intrusive_ptr<Buffer>;
// -- constructors, destructors, and assignment operators --------------------
from_resource_sub(coordinator* ctx, buffer_ptr buf, observer<value_type> out)
: ctx_(ctx), buf_(buf), out_(std::move(out)) {
ctx_->ref_coordinator();
}
~from_resource_sub() {
if (buf_)
buf_->cancel();
ctx_->deref_coordinator();
}
// -- implementation of subscription_impl ------------------------------------
bool disposed() const noexcept override {
return disposed_;
}
void cancel() override {
CAF_LOG_TRACE("");
if (!disposed_) {
disposed_ = true;
if (!running_)
do_cancel();
}
}
void request(size_t n) override {
CAF_LOG_TRACE(CAF_ARG(n));
if (demand_ != 0) {
demand_ += n;
} else {
demand_ = n;
run_later();
}
}
// -- implementation of async::consumer: these may get called concurrently ---
void on_producer_ready() override {
// nop
}
void on_producer_wakeup() override {
CAF_LOG_TRACE("");
ctx_->schedule_fn([ptr = strong_this()] {
CAF_LOG_TRACE("");
ptr->running_ = true;
ptr->do_run();
});
}
void ref_consumer() const noexcept override {
this->ref();
}
void deref_consumer() const noexcept override {
this->deref();
}
private:
void run_later() {
if (!running_) {
running_ = true;
ctx_->delay_fn([ptr = strong_this()] { ptr->do_run(); });
}
}
void do_cancel() {
if (buf_) {
buf_->cancel();
buf_ = nullptr;
}
if (out_) {
out_.on_complete();
out_ = nullptr;
}
}
void do_run() {
CAF_LOG_TRACE("");
auto guard = detail::make_scope_guard([this] { running_ = false; });
if (disposed_) {
do_cancel();
return;
}
CAF_ASSERT(out_);
CAF_ASSERT(buf_);
while (demand_ > 0) {
auto [again, pulled] = buf_->pull(async::delay_errors, demand_, out_);
if (!again) {
buf_ = nullptr;
out_ = nullptr;
disposed_ = true;
return;
} else if (disposed_) {
do_cancel();
return;
} else if (pulled == 0) {
return;
} else {
CAF_ASSERT(demand_ >= pulled);
demand_ -= pulled;
}
}
}
intrusive_ptr<from_resource_sub> strong_this() {
return {this};
}
/// Stores the context (coordinator) that runs this flow. Unlike other
/// observables, we need a strong reference to the context because otherwise
/// the buffer might call schedule_fn on a destroyed object.
intrusive_ptr<coordinator> ctx_;
/// Stores a pointer to the asynchronous input buffer.
buffer_ptr buf_;
/// Stores a pointer to the target observer running on `remote_ctx_`.
observer<value_type> out_;
/// Stores whether do_run is currently running.
bool running_ = false;
/// Stores whether dispose() has been called.
bool disposed_ = false;
/// Stores the demand from the observer.
size_t demand_ = 0;
};
template <class T>
class from_resource : public hot<T> {
public:
// -- member types -----------------------------------------------------------
using resource_type = async::consumer_resource<T>;
using super = hot<T>;
// -- constructors, destructors, and assignment operators --------------------
from_resource(coordinator* ctx, resource_type resource)
: super(ctx), resource_(std::move(resource)) {
// nop
}
// -- implementation of observable_impl<T> -----------------------------------
disposable subscribe(observer<T> out) override {
CAF_LOG_TRACE("");
CAF_ASSERT(out);
if (resource_) {
if (auto buf = resource_.try_open()) {
resource_ = nullptr;
using buffer_type = typename resource_type::buffer_type;
CAF_LOG_DEBUG("add subscriber");
using impl_t = from_resource_sub<buffer_type>;
auto ptr = make_counted<impl_t>(super::ctx_, buf, out);
buf->set_consumer(ptr);
super::ctx_->watch(ptr->as_disposable());
out.on_subscribe(subscription{ptr});
return ptr->as_disposable();
} else {
resource_ = nullptr;
CAF_LOG_WARNING("failed to open an async resource");
auto err = make_error(sec::cannot_open_resource,
"failed to open an async resource");
out.on_error(err);
return disposable{};
}
} else {
CAF_LOG_WARNING("may only subscribe once to an async resource");
auto err = make_error(sec::too_many_observers,
"may only subscribe once to an async resource");
out.on_error(err);
return disposable{};
}
}
private:
resource_type resource_;
};
} // namespace caf::flow::op
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/defaults.hpp"
#include "caf/detail/ref_counted_base.hpp"
#include "caf/detail/scope_guard.hpp"
#include "caf/detail/type_list.hpp"
#include <deque>
#include <tuple>
#include <utility>
namespace caf::flow::op {
template <class... Steps>
using from_steps_output_t =
typename detail::tl_back_t<detail::type_list<Steps...>>::output_type;
template <class Input, class... Steps>
class from_steps_sub : public detail::ref_counted_base,
public observer_impl<Input>,
public subscription_impl {
public:
// -- member types -----------------------------------------------------------
using output_type = from_steps_output_t<Steps...>;
struct term_step;
friend struct term_step;
struct term_step {
from_steps_sub* sub;
bool on_next(const output_type& next) {
sub->buf_.push_back(next);
return true;
}
void on_complete() {
CAF_ASSERT(sub->in_.valid());
sub->in_.cancel();
sub->in_ = nullptr;
}
void on_error(const error& what) {
CAF_ASSERT(sub->subscribed());
sub->in_.cancel();
sub->in_ = nullptr;
sub->err_ = what;
}
};
// -- constructors, destructors, and assignment operators --------------------
from_steps_sub(coordinator* ctx, observer<output_type> out,
std::tuple<Steps...> steps)
: ctx_(ctx), out_(std::move(out)), steps_(std::move(steps)) {
// nop
}
// -- ref counting -----------------------------------------------------------
void ref_disposable() const noexcept final {
this->ref();
}
void deref_disposable() const noexcept final {
this->deref();
}
void ref_coordinated() const noexcept final {
this->ref();
}
void deref_coordinated() const noexcept final {
this->deref();
}
friend void intrusive_ptr_add_ref(const from_steps_sub* ptr) noexcept {
ptr->ref();
}
friend void intrusive_ptr_release(const from_steps_sub* ptr) noexcept {
ptr->deref();
}
// -- properties -------------------------------------------------------------
bool subscribed() const noexcept {
return in_.valid();
}
const error& fail_reason() const {
return err_;
}
bool idle() {
return demand_ > 0 && buf_.empty();
}
// -- implementation of observer_impl<Input> ---------------------------------
void on_next(const Input& item) override {
CAF_ASSERT(!in_ || in_flight_ > 0);
if (in_) {
--in_flight_;
auto fn = [this, &item](auto& step, auto&... steps) {
term_step term{this};
step.on_next(item, steps..., term);
};
std::apply(fn, steps_);
pull();
if (!running_) {
running_ = true;
do_run();
}
}
}
void on_complete() override {
if (in_) {
auto fn = [this](auto& step, auto&... steps) {
term_step term{this};
step.on_complete(steps..., term);
};
std::apply(fn, steps_);
if (!running_) {
running_ = true;
do_run();
}
}
}
void on_error(const error& what) override {
if (in_) {
auto fn = [this, &what](auto& step, auto&... steps) {
term_step term{this};
step.on_error(what, steps..., term);
};
std::apply(fn, steps_);
if (!running_) {
running_ = true;
do_run();
}
}
}
void on_subscribe(subscription in) override {
if (!in_) {
in_ = std::move(in);
pull();
} else {
in.cancel();
}
}
// -- implementation of subscription_impl ------------------------------------
bool disposed() const noexcept override {
return disposed_;
}
void cancel() override {
CAF_LOG_TRACE("");
if (!disposed_) {
disposed_ = true;
demand_ = 0;
buf_.clear();
ctx_->delay_fn([out = std::move(out_)]() mutable { out.on_complete(); });
}
if (in_) {
in_.cancel();
in_ = nullptr;
}
}
void request(size_t n) override {
CAF_LOG_TRACE(CAF_ARG(n));
if (demand_ != 0) {
demand_ += n;
} else {
demand_ = n;
run_later();
}
}
private:
void pull() {
if (auto pending = buf_.size() + in_flight_;
in_ && pending < max_buf_size_) {
auto new_demand = max_buf_size_ - pending;
in_flight_ += new_demand;
in_.request(new_demand);
}
}
void run_later() {
if (!running_) {
running_ = true;
ctx_->delay_fn([ptr = strong_this()] { ptr->do_run(); });
}
}
void do_run() {
auto guard = detail::make_scope_guard([this] { running_ = false; });
if (!disposed_) {
CAF_ASSERT(out_);
while (demand_ > 0 && !buf_.empty()) {
auto item = std::move(buf_.front());
buf_.pop_front();
--demand_;
out_.on_next(item);
}
if (!in_ && buf_.empty()) {
if (err_)
out_.on_error(err_);
else
out_.on_complete();
out_ = nullptr;
disposed_ = true;
}
}
}
intrusive_ptr<from_steps_sub> strong_this() {
return {this};
}
coordinator* ctx_;
subscription in_;
observer<output_type> out_;
std::tuple<Steps...> steps_;
std::deque<output_type> buf_;
size_t demand_ = 0;
size_t in_flight_ = 0;
size_t max_buf_size_ = defaults::flow::buffer_size;
bool disposed_ = false;
bool running_ = false;
error err_;
};
template <class Input, class... Steps>
class from_steps : public op::cold<from_steps_output_t<Steps...>> {
public:
// -- member types -----------------------------------------------------------
static_assert(sizeof...(Steps) > 0);
using input_type = Input;
using output_type = from_steps_output_t<Steps...>;
using super = op::cold<output_type>;
// -- constructors, destructors, and assignment operators --------------------
from_steps(coordinator* ctx, intrusive_ptr<base<input_type>> input,
std::tuple<Steps...> steps)
: super(ctx), input_(std::move(input)), steps_(std::move(steps)) {
// nop
}
// -- implementation of observable_impl<T> -----------------------------------
disposable subscribe(observer<output_type> out) override {
CAF_LOG_TRACE(CAF_ARG2("out", out.ptr()));
using sub_t = from_steps_sub<Input, Steps...>;
auto ptr = make_counted<sub_t>(super::ctx_, out, steps_);
input_->subscribe(observer<input_type>{ptr});
if (ptr->subscribed()) {
auto sub = subscription{std::move(ptr)};
out.on_subscribe(sub);
return std::move(sub).as_disposable();
} else if (auto& fail_reason = ptr->fail_reason()) {
out.on_error(fail_reason);
return disposable{};
} else {
auto err = make_error(sec::invalid_observable,
"flow operator from_steps failed "
"to subscribe to its input");
out.on_error(err);
return disposable{};
}
}
private:
intrusive_ptr<base<input_type>> input_;
std::tuple<Steps...> steps_;
};
} // namespace caf::flow::op
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/detail/ref_counted_base.hpp"
#include "caf/flow/observer.hpp"
#include "caf/flow/op/base.hpp"
#include "caf/flow/subscription.hpp"
namespace caf::flow::op {
/// Convenience base type for *hot* observable types.
template <class T>
class hot : public detail::ref_counted_base, public base<T> {
public:
// -- member types -----------------------------------------------------------
using output_type = T;
// -- constructors, destructors, and assignment operators --------------------
explicit hot(coordinator* ctx) : ctx_(ctx) {
// nop
}
// -- implementation of disposable_impl --------------------------------------
void ref_coordinated() const noexcept final {
this->ref();
}
void deref_coordinated() const noexcept final {
this->deref();
}
// -- implementation of observable_impl<T> -----------------------------------
coordinator* ctx() const noexcept final {
return ctx_;
}
protected:
// -- member variables -------------------------------------------------------
coordinator* ctx_;
};
} // namespace caf::flow::op
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/detail/core_export.hpp"
#include "caf/flow/observer.hpp"
#include "caf/flow/op/cold.hpp"
#include "caf/timespan.hpp"
#include <cstdint>
namespace caf::flow::op {
class CAF_CORE_EXPORT interval : public cold<int64_t> {
public:
// -- member types -----------------------------------------------------------
using super = cold<int64_t>;
// -- constructors, destructors, and assignment operators --------------------
interval(coordinator* ctx, timespan initial_delay, timespan period);
interval(coordinator* ctx, timespan initial_delay, timespan period,
int64_t max_val);
// -- implementation of observable_impl<T> -----------------------------------
disposable subscribe(observer<int64_t> out) override;
private:
timespan initial_delay_;
timespan period_;
int64_t max_;
};
} // namespace caf::flow::op
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/flow/coordinator.hpp"
#include "caf/flow/observer.hpp"
#include "caf/flow/op/empty.hpp"
#include "caf/flow/op/hot.hpp"
#include "caf/flow/subscription.hpp"
#include "caf/make_counted.hpp"
#include <algorithm>
#include <deque>
#include <memory>
namespace caf::flow::op {
/// State shared between one multicast operator and one subscribed observer.
template <class T>
struct mcast_sub_state {
std::deque<T> buf;
size_t demand = 0;
observer<T> out;
bool disposed = false;
bool closed = false;
bool running = false;
error err;
void push(const T& item) {
if (disposed) {
// nop
} else if (demand > 0 && !running) {
CAF_ASSERT(out);
CAF_ASSERT(buf.empty());
--demand;
out.on_next(item);
} else {
buf.push_back(item);
}
}
void close() {
if (!disposed) {
closed = true;
if (!running && buf.empty()) {
disposed = true;
out.on_complete();
}
}
}
void abort(const error& reason) {
if (!disposed && !err) {
closed = true;
err = reason;
if (!running && buf.empty()) {
disposed = true;
out.on_error(reason);
}
}
}
void do_dispose() {
if (out) {
out.on_complete();
out = nullptr;
}
buf.clear();
demand = 0;
disposed = true;
}
void do_run() {
auto guard = detail::make_scope_guard([this] { running = false; });
if (!disposed) {
while (demand > 0 && !buf.empty()) {
out.on_next(buf.front());
buf.pop_front();
--demand;
}
if (buf.empty() && closed) {
disposed = true;
if (err)
out.on_error(err);
else
out.on_complete();
}
}
}
};
template <class T>
using mcast_sub_state_ptr = std::shared_ptr<mcast_sub_state<T>>;
template <class T>
class mcast_sub : public subscription::impl_base {
public:
// -- constructors, destructors, and assignment operators --------------------
mcast_sub(coordinator* ctx, mcast_sub_state_ptr<T> state)
: ctx_(ctx), state_(std::move(state)) {
// nop
}
// -- implementation of subscription -----------------------------------------
bool disposed() const noexcept override {
return !state_;
}
void cancel() override {
if (state_) {
ctx_->delay_fn([state = std::move(state_)]() { state->do_dispose(); });
}
}
void request(size_t n) override {
state_->demand += n;
if (!state_->running) {
state_->running = true;
ctx_->delay_fn([state = state_] { state->do_run(); });
}
}
private:
/// Stores the context (coordinator) that runs this flow.
coordinator* ctx_;
/// Stores a handle to the state.
mcast_sub_state_ptr<T> state_;
};
// Base type for *hot* operators that multicast data to subscribed observers.
template <class T>
class mcast : public hot<T> {
public:
// -- member types -----------------------------------------------------------
using super = hot<T>;
using state_type = mcast_sub_state<T>;
using state_ptr_type = mcast_sub_state_ptr<T>;
using observer_type = observer<T>;
// -- constructors, destructors, and assignment operators --------------------
explicit mcast(coordinator* ctx) : super(ctx) {
// nop
}
/// Pushes @p item to all subscribers.
void push_all(const T& item) {
for (auto& state : states_)
state->push(item);
}
/// Closes the operator, eventually emitting on_complete on all observers.
void close() {
if (!closed_) {
closed_ = true;
for (auto& state : states_)
state->close();
states_.clear();
}
}
/// Closes the operator, eventually emitting on_error on all observers.
void abort(const error& reason) {
if (!closed_) {
closed_ = true;
for (auto& state : states_)
state->abort(reason);
states_.clear();
}
}
size_t max_demand() const noexcept {
if (states_.empty()) {
return 0;
} else {
auto pred = [](const state_ptr_type& x, const state_ptr_type& y) {
return x->demand < y->demand;
};
return std::max_element(states_.begin(), states_.end(), pred)->demand;
}
}
size_t min_demand() const noexcept {
if (states_.empty()) {
return 0;
} else {
auto pred = [](const state_ptr_type& x, const state_ptr_type& y) {
return x->demand < y->demand;
};
return std::min_element(states_.begin(), states_.end(), pred)->demand;
}
}
size_t max_buffered() const noexcept {
if (states_.empty()) {
return 0;
} else {
auto pred = [](const state_ptr_type& x, const state_ptr_type& y) {
return x->buf.size() < y->buf.size();
};
return std::max_element(states_.begin(), states_.end(), pred)->buf.size();
}
}
size_t min_buffered() const noexcept {
if (states_.empty()) {
return 0;
} else {
auto pred = [](const state_ptr_type& x, const state_ptr_type& y) {
return x->buf.size() < y->buf.size();
};
return std::min_element(states_.begin(), states_.end(), pred)->buf.size();
}
}
/// Queries whether there is at least one observer subscribed to the operator.
bool has_observers() const noexcept {
return !states_.empty();
}
state_ptr_type add_state(observer_type out) {
auto state = std::make_shared<state_type>();
state->out = std::move(out);
states_.push_back(state);
return state;
}
disposable subscribe(observer<T> out) override {
if (!closed_) {
auto ptr = make_counted<mcast_sub<T>>(super::ctx_, add_state(out));
out.on_subscribe(subscription{ptr});
return disposable{std::move(ptr)};
} else if (!err_) {
return make_counted<op::empty<T>>(super::ctx_)->subscribe(out);
} else {
out.on_error(err_);
return disposable{};
}
}
protected:
bool closed_ = false;
error err_;
std::vector<mcast_sub_state_ptr<T>> states_;
};
} // namespace caf::flow::op
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/detail/ref_counted_base.hpp"
#include "caf/flow/observer.hpp"
#include "caf/flow/op/cold.hpp"
#include "caf/flow/op/empty.hpp"
#include "caf/flow/subscription.hpp"
#include "caf/make_counted.hpp"
#include <deque>
#include <memory>
#include <numeric>
#include <utility>
#include <variant>
#include <vector>
namespace caf::flow::op {
/// @relates merge
template <class T>
struct merge_input {
/// The subscription to this input.
subscription sub;
/// Stores received items until the merge can forward them downstream.
std::deque<T> buf;
};
template <class T>
class merge_sub : public subscription::impl_base {
public:
// -- member types -----------------------------------------------------------
using input_t = merge_input<T>;
using input_key = size_t;
using input_ptr = std::unique_ptr<input_t>;
using input_map = detail::unordered_flat_map<input_key, input_ptr>;
// -- constructors, destructors, and assignment operators --------------------
merge_sub(coordinator* ctx, observer<T> out)
: ctx_(ctx), out_(std::move(out)) {
// nop
}
// -- input management -------------------------------------------------------
void subscribe_to(observable<T> what) {
auto key = next_key_++;
inputs_.container().emplace_back(key, std::make_unique<input_t>());
using fwd_impl = forwarder<T, merge_sub, size_t>;
auto fwd = make_counted<fwd_impl>(this, key);
what.subscribe(fwd->as_observer());
}
void subscribe_to(observable<observable<T>> what) {
auto key = next_key_++;
auto& vec = inputs_.container();
vec.emplace_back(key, std::make_unique<input_t>());
using fwd_impl = forwarder<observable<T>, merge_sub, size_t>;
auto fwd = make_counted<fwd_impl>(this, key);
what.subscribe(fwd->as_observer());
}
// -- callbacks for the forwarders -------------------------------------------
void fwd_on_subscribe(input_key key, subscription sub) {
CAF_LOG_TRACE(CAF_ARG(key));
if (auto ptr = get(key); ptr && !ptr->sub && out_) {
sub.request(max_pending_);
ptr->sub = std::move(sub);
} else {
sub.cancel();
}
}
void fwd_on_complete(input_key key) {
CAF_LOG_TRACE(CAF_ARG(key));
if (auto i = inputs_.find(key); i != inputs_.end()) {
if (i->second->buf.empty()) {
inputs_.erase(i);
run_later();
} else {
i->second->sub = nullptr;
}
}
}
void fwd_on_error(input_key key, const error& what) {
CAF_LOG_TRACE(CAF_ARG(key) << CAF_ARG(what));
if (!err_) {
err_ = what;
if (!flags_.delay_error) {
auto i = inputs_.begin();
while (i != inputs_.end()) {
auto& input = *i->second;
if (auto& sub = input.sub) {
sub.cancel();
sub = nullptr;
}
if (input.buf.empty())
i = inputs_.erase(i);
else
++i;
}
}
}
fwd_on_complete(key);
}
void fwd_on_next(input_key key, const T& item) {
CAF_LOG_TRACE(CAF_ARG(key) << CAF_ARG(item));
if (auto ptr = get(key)) {
ptr->buf.push_back(item);
if (ptr->buf.size() == 1 && !flags_.running) {
flags_.running = true;
do_run();
}
}
}
void fwd_on_next(input_key key, const observable<T>& item) {
CAF_LOG_TRACE(CAF_ARG(key) << CAF_ARG(item));
if (auto ptr = get(key))
subscribe_to(item);
}
// -- implementation of subscription_impl ------------------------------------
bool disposed() const noexcept override {
return !out_;
}
void cancel() override {
if (out_) {
for (auto& kvp : inputs_)
if (auto& sub = kvp.second->sub)
sub.cancel();
inputs_.clear();
run_later();
}
}
void request(size_t n) override {
CAF_ASSERT(out_.valid());
demand_ += n;
run_later();
}
size_t buffered() const noexcept {
return std::accumulate(inputs_.begin(), inputs_.end(), size_t{0},
[](size_t n, auto& kvp) {
return n + kvp.second->buf.size();
});
}
private:
void run_later() {
if (!flags_.running) {
flags_.running = true;
ctx_->delay_fn([strong_this = intrusive_ptr<merge_sub>{this}] {
strong_this->do_run();
});
}
}
auto next_input() {
CAF_ASSERT(!inputs_.empty());
auto has_items_at = [this](size_t pos) {
auto& vec = inputs_.container();
return !vec[pos].second->buf.empty();
};
auto start = pos_ % inputs_.size();
pos_ = (pos_ + 1) % inputs_.size();
if (has_items_at(start))
return inputs_.begin() + start;
while (pos_ != start) {
if (has_items_at(pos_))
return inputs_.begin() + pos_;
pos_ = (pos_ + 1) % inputs_.size();
}
return inputs_.end();
}
void do_run() {
while (out_ && demand_ > 0 && !inputs_.empty()) {
if (auto i = next_input(); i != inputs_.end()) {
--demand_;
auto& buf = i->second->buf;
auto tmp = std::move(buf.front());
buf.pop_front();
if (auto& sub = i->second->sub)
sub.request(1);
else if (buf.empty())
inputs_.erase(i);
out_.on_next(tmp);
} else {
flags_.running = false;
return;
}
}
if (out_ && inputs_.empty())
fin();
flags_.running = false;
}
void fin() {
if (!inputs_.empty()) {
for (auto& kvp : inputs_)
if (auto& sub = kvp.second->sub)
sub.cancel();
inputs_.clear();
}
if (!err_) {
out_.on_complete();
} else {
out_.on_error(err_);
}
out_ = nullptr;
}
/// Selects an input object by key or returns null.
input_t* get(input_key key) {
if (auto i = inputs_.find(key); i != inputs_.end())
return i->second.get();
else
return nullptr;
}
/// Groups various Boolean flags.
struct flags_t {
/// Configures whether an error immediately aborts the merging or not.
bool delay_error : 1;
/// Stores whether the merge is currently executing do_run.
bool running : 1;
flags_t() : delay_error(false), running(false) {
// nop
}
};
/// Stores the context (coordinator) that runs this flow.
coordinator* ctx_;
/// Stores the first error that occurred on any input.
error err_;
/// Fine-tunes the behavior of the merge.
flags_t flags_;
/// Stores our current demand for items from the subscriber.
size_t demand_ = 0;
/// Stores a handle to the subscriber.
observer<T> out_;
/// Stores the last round-robin read position.
size_t pos_ = 0;
/// Associates inputs with ascending keys.
input_map inputs_;
/// Stores the key for the next input.
size_t next_key_ = 0;
/// Configures how many items we buffer per input.
size_t max_pending_ = defaults::flow::buffer_size;
};
template <class T>
class merge : public cold<T> {
public:
// -- member types -----------------------------------------------------------
using super = cold<T>;
using input_type = std::variant<observable<T>, observable<observable<T>>>;
// -- constructors, destructors, and assignment operators --------------------
template <class... Ts, class... Inputs>
explicit merge(coordinator* ctx, Inputs&&... inputs) : super(ctx) {
(add(std::forward<Inputs>(inputs)), ...);
}
// -- properties -------------------------------------------------------------
size_t inputs() const noexcept {
return inputs_.size();
}
// -- implementation of observable_impl<T> -----------------------------------
disposable subscribe(observer<T> out) override {
if (inputs() == 0) {
auto ptr = make_counted<empty<T>>(super::ctx_);
return ptr->subscribe(std::move(out));
} else {
auto sub = make_counted<merge_sub<T>>(super::ctx_, out);
for (auto& input : inputs_)
std::visit([&sub](auto& in) { sub->subscribe_to(in); }, input);
out.on_subscribe(subscription{sub});
return sub->as_disposable();
}
}
private:
template <class Input>
void add(Input&& x) {
using input_t = std::decay_t<Input>;
if constexpr (detail::is_iterable_v<input_t>) {
for (auto& in : x)
add(in);
} else {
static_assert(is_observable_v<input_t>);
inputs_.emplace_back(std::forward<Input>(x).as_observable());
}
}
std::vector<input_type> inputs_;
};
} // namespace caf::flow::op
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/flow/observer.hpp"
#include "caf/flow/op/cold.hpp"
#include "caf/flow/subscription.hpp"
#include <utility>
namespace caf::flow::op {
template <class T>
class never_sub : public subscription::impl_base {
public:
// -- constructors, destructors, and assignment operators --------------------
never_sub(coordinator* ctx, observer<T> out)
: ctx_(ctx), out_(std::move(out)) {
// nop
}
// -- implementation of subscription -----------------------------------------
bool disposed() const noexcept override {
return !out_;
}
void cancel() override {
if (out_)
ctx_->delay_fn([out = std::move(out_)]() mutable { out.on_complete(); });
}
void request(size_t) override {
// nop
}
private:
/// Stores the context (coordinator) that runs this flow.
coordinator* ctx_;
/// Stores a handle to the subscribed observer.
observer<T> out_;
};
/// An observable that never calls any callbacks on its subscribers.
template <class T>
class never : public cold<T> {
public:
// -- member types -----------------------------------------------------------
using super = cold<T>;
using output_type = T;
// -- constructors, destructors, and assignment operators --------------------
explicit never(coordinator* ctx) : super(ctx) {
// nop
}
// -- implementation of observable_impl<T> -----------------------------------
disposable subscribe(observer<output_type> out) override {
auto ptr = make_counted<never_sub<T>>(super::ctx_, out);
out.ptr()->on_subscribe(subscription{ptr});
return ptr->as_disposable();
}
};
} // namespace caf::flow::op
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/cow_tuple.hpp"
#include "caf/flow/observable.hpp"
#include "caf/flow/observable_decl.hpp"
#include "caf/flow/observer.hpp"
#include "caf/flow/op/cell.hpp"
#include "caf/flow/subscription.hpp"
#include <cstdint>
namespace caf::flow::op {
template <class T>
class pipe_sub : public detail::ref_counted_base,
public observer_impl<T>,
public subscription_impl {
public:
// -- constructors, destructors, and assignment operators --------------------
explicit pipe_sub(subscription in) : in_(std::move(in)) {
// nop
}
void init(observer<T> out) {
if (!completed_)
out_ = std::move(out);
else if (err_)
out.on_error(err_);
else
out.on_complete();
}
// -- ref counting -----------------------------------------------------------
void ref_coordinated() const noexcept final {
this->ref();
}
void deref_coordinated() const noexcept final {
this->deref();
}
void ref_disposable() const noexcept final {
this->ref();
}
void deref_disposable() const noexcept final {
this->deref();
}
friend void intrusive_ptr_add_ref(const pipe_sub* ptr) noexcept {
ptr->ref();
}
friend void intrusive_ptr_release(const pipe_sub* ptr) noexcept {
ptr->deref();
}
// -- implementation of observer_impl<Input> ---------------------------------
void on_next(const T& item) override {
if (out_)
out_.on_next(item);
}
void on_complete() override {
if (out_) {
in_ = nullptr;
out_.on_complete();
out_ = nullptr;
completed_ = true;
} else if (!completed_) {
completed_ = true;
}
}
void on_error(const error& what) override {
if (out_) {
in_ = nullptr;
out_.on_error(what);
out_ = nullptr;
completed_ = true;
err_ = what;
} else if (!completed_) {
completed_ = true;
err_ = what;
}
}
void on_subscribe(subscription in) override {
in.cancel();
}
// -- implementation of subscription_impl ------------------------------------
bool disposed() const noexcept override {
return !out_;
}
void cancel() override {
if (in_) {
in_.cancel();
in_ = nullptr;
}
if (out_) {
out_.on_complete();
out_ = nullptr;
}
}
void request(size_t n) override {
if (in_) {
in_.request(n);
}
}
private:
subscription in_;
observer<T> out_;
/// Stores whether on_complete has been called. This may be the case even
/// before init gets called.
bool completed_ = false;
/// Stores the error passed to on_error.
error err_;
};
template <class T>
class pipe : public hot<T> {
public:
using super = hot<T>;
using sub_ptr = intrusive_ptr<pipe_sub<T>>;
explicit pipe(coordinator* ctx, sub_ptr sub)
: super(ctx), sub_(std::move(sub)) {
// nop
}
~pipe() {
if (sub_)
sub_->cancel();
}
disposable subscribe(observer<T> out) override {
if (sub_) {
sub_->init(out);
out.on_subscribe(subscription{sub_});
return disposable{std::move(sub_)};
} else {
auto err = make_error(sec::invalid_observable,
"pipes may only be subscribed once");
out.on_error(err);
return disposable{};
}
}
private:
sub_ptr sub_;
};
template <class T>
class prefix_and_tail_fwd : public detail::ref_counted_base,
public observer_impl<T> {
public:
// -- constructors, destructors, and assignment operators --------------------
using output_type = cow_tuple<std::vector<T>, observable<T>>;
using cell_ptr = intrusive_ptr<cell<output_type>>;
prefix_and_tail_fwd(coordinator* ctx, size_t prefix_size, cell_ptr sync)
: ctx_(ctx), prefix_size_(prefix_size), sync_(std::move(sync)) {
// nop
}
// -- ref counting -----------------------------------------------------------
void ref_coordinated() const noexcept final {
this->ref();
}
void deref_coordinated() const noexcept final {
this->deref();
}
friend void intrusive_ptr_add_ref(const prefix_and_tail_fwd* ptr) noexcept {
ptr->ref();
}
friend void intrusive_ptr_release(const prefix_and_tail_fwd* ptr) noexcept {
ptr->deref();
}
// -- implementation of observer_impl<T> -------------------------------------
void on_next(const T& item) override {
if (next_) {
next_.on_next(item);
} else if (sync_) {
prefix_.push_back(item);
if (prefix_.size() == prefix_size_) {
auto fwd = make_counted<pipe_sub<T>>(std::move(in_));
auto tmp = make_counted<pipe<T>>(ctx_, fwd);
output_type result{std::move(prefix_), observable<T>{tmp}};
sync_->set_value(std::move(result));
sync_ = nullptr;
next_ = observer<T>{fwd};
}
}
}
void on_complete() override {
if (next_) {
next_.on_complete();
next_ = nullptr;
} else if (sync_) {
sync_->set_null();
sync_ = nullptr;
}
}
void on_error(const error& what) override {
if (next_) {
next_.on_error(what);
next_ = nullptr;
} else if (sync_) {
sync_->set_error(what);
sync_ = nullptr;
}
}
void on_subscribe(subscription in) override {
in_ = std::move(in);
in_.request(prefix_size_);
}
private:
coordinator* ctx_;
size_t prefix_size_ = 0;
subscription in_;
std::vector<T> prefix_;
observer<T> next_;
cell_ptr sync_;
};
template <class T>
class prefix_and_tail : public cold<cow_tuple<std::vector<T>, observable<T>>> {
public:
// -- member types -----------------------------------------------------------
using output_type = cow_tuple<std::vector<T>, observable<T>>;
using super = cold<output_type>;
using observer_type = observer<T>;
// -- constructors, destructors, and assignment operators --------------------
prefix_and_tail(coordinator* ctx, intrusive_ptr<base<T>> decorated,
size_t prefix_size)
: super(ctx), decorated_(std::move(decorated)), prefix_size_(prefix_size) {
// nop
}
disposable subscribe(observer<output_type> out) override {
// The cell acts as a sort of handshake between the dispatcher and the
// observer. After producing the (prefix, tail) pair, the dispatcher goes on
// to forward items from the decorated observable to the tail.
auto sync = make_counted<cell<output_type>>(super::ctx_);
auto fwd = make_counted<prefix_and_tail_fwd<T>>(super::ctx_, prefix_size_,
sync);
std::vector<disposable> result;
result.reserve(2);
result.emplace_back(sync->subscribe(std::move(out)));
result.emplace_back(decorated_->subscribe(observer<T>{std::move(fwd)}));
return disposable::make_composite(std::move(result));
}
private:
intrusive_ptr<base<T>> decorated_;
size_t prefix_size_;
};
} // namespace caf::flow::op
...@@ -4,97 +4,90 @@ ...@@ -4,97 +4,90 @@
#pragma once #pragma once
#include "caf/async/policy.hpp" #include "caf/detail/ref_counted_base.hpp"
#include "caf/flow/observable.hpp" #include "caf/flow/observable_decl.hpp"
#include "caf/flow/observable_state.hpp"
#include "caf/flow/observer.hpp" #include "caf/flow/observer.hpp"
#include "caf/flow/op/cold.hpp"
#include "caf/flow/op/empty.hpp"
#include "caf/flow/subscription.hpp"
#include <algorithm>
#include <memory>
#include <tuple>
#include <type_traits> #include <type_traits>
#include <utility> #include <utility>
#include <vector>
namespace caf::flow { namespace caf::flow::op {
template <class F, class... Ts> template <class F, class... Ts>
struct zipper_oracle { struct zip_wich_oracle {
using output_type using output_type
= decltype(std::declval<F&>()(std::declval<const Ts&>()...)); = decltype(std::declval<F&>()(std::declval<const Ts&>()...));
}; };
template <class F, class... Ts> template <class F, class... Ts>
using zipper_output_t = typename zipper_oracle<F, Ts...>::output_type; using zip_with_output_t = typename zip_wich_oracle<F, Ts...>::output_type;
template <size_t Index> template <size_t Index>
using zipper_index = std::integral_constant<size_t, Index>; using zip_index = std::integral_constant<size_t, Index>;
template <class T> template <class T>
struct zipper_input { struct zip_input {
using value_type = T; using value_type = T;
explicit zipper_input(observable<T> in) : in(std::move(in)) {
// nop
}
observable<T> in;
subscription sub; subscription sub;
std::vector<T> buf; std::vector<T> buf;
bool broken = false;
/// Returns whether the input can no longer produce additional items. /// Returns whether the input can no longer produce additional items.
bool at_end() const noexcept { bool at_end() const noexcept {
return broken && buf.empty(); return !sub && buf.empty();
} }
}; };
/// Combines items from any number of observables using a zip function. /// Combines items from any number of observables using a zip function.
template <class F, class... Ts> template <class F, class... Ts>
class zipper_impl : public observable_impl_base<zipper_output_t<F, Ts...>> { class zip_with_sub : public subscription::impl_base {
public: public:
// -- member types ----------------------------------------------------------- // -- member types -----------------------------------------------------------
using output_type = zipper_output_t<F, Ts...>; using output_type = zip_with_output_t<F, Ts...>;
using super = observable_impl_base<output_type>;
// -- friends ----------------------------------------------------------------
CAF_INTRUSIVE_PTR_FRIENDS(zipper_impl)
template <class, class, class>
friend class forwarder;
// -- constructors, destructors, and assignment operators -------------------- // -- constructors, destructors, and assignment operators --------------------
zipper_impl(coordinator* ctx, F fn, observable<Ts>... inputs) zip_with_sub(coordinator* ctx, F fn, observer<output_type> out,
: super(ctx), fn_(std::move(fn)), inputs_(inputs...) { std::tuple<observable<Ts>...>& srcs)
// nop : ctx_(ctx), fn_(std::move(fn)), out_(std::move(out)) {
for_each_input([this, &srcs](auto index, auto& input) {
using index_type = decltype(index);
using value_type = typename std::decay_t<decltype(input)>::value_type;
using fwd_impl = forwarder<value_type, zip_with_sub, index_type>;
auto fwd = make_counted<fwd_impl>(this, index);
std::get<index_type::value>(srcs).subscribe(fwd->as_observer());
});
} }
// -- implementation of disposable::impl ------------------------------------- // -- implementation of subscription -----------------------------------------
void dispose() override { bool disposed() const noexcept override {
if (buffered() == 0) { return !out_;
fin(); }
} else {
void cancel() override {
if (out_) {
for_each_input([](auto, auto& input) { for_each_input([](auto, auto& input) {
input.broken = true;
input.in = nullptr;
if (input.sub) { if (input.sub) {
input.sub.cancel(); input.sub.cancel();
input.sub = nullptr; input.sub = nullptr;
} }
// Do not clear the buffer to allow already arrived items to go through. input.buf.clear();
}); });
fin();
} }
} }
bool disposed() const noexcept override { void request(size_t n) override {
return term_.finalized(); if (out_) {
}
// -- implementation of observable<T>::impl ----------------------------------
void on_request(disposable_impl* sink, size_t demand) override {
if (auto n = term_.on_request(sink, demand); n > 0) {
demand_ += n; demand_ += n;
for_each_input([n](auto, auto& input) { for_each_input([n](auto, auto& input) {
if (input.sub) if (input.sub)
...@@ -103,40 +96,16 @@ public: ...@@ -103,40 +96,16 @@ public:
} }
} }
void on_cancel(disposable_impl* sink) override { // -- utility functions ------------------------------------------------------
if (auto n = term_.on_cancel(sink); n > 0) {
demand_ += n;
for_each_input([n](auto, auto& input) {
if (input.sub)
input.sub.request(n);
});
}
}
disposable subscribe(observer<output_type> sink) override {
// On the first subscribe, we subscribe to our inputs.
auto res = term_.add(this, sink);
if (res && term_.start()) {
for_each_input([this](auto index, auto& input) {
using input_t = std::decay_t<decltype(input)>;
using value_t = typename input_t::value_type;
using fwd_impl = forwarder<value_t, zipper_impl, decltype(index)>;
auto fwd = make_counted<fwd_impl>(this, index);
input.in.subscribe(fwd->as_observer());
});
}
return res;
}
private:
template <size_t I> template <size_t I>
auto& at(zipper_index<I>) { auto& at(zip_index<I>) {
return std::get<I>(inputs_); return std::get<I>(inputs_);
} }
template <class Fn, size_t... Is> template <class Fn, size_t... Is>
void for_each_input(Fn&& fn, std::index_sequence<Is...>) { void for_each_input(Fn&& fn, std::index_sequence<Is...>) {
(fn(zipper_index<Is>{}, at(zipper_index<Is>{})), ...); (fn(zip_index<Is>{}, at(zip_index<Is>{})), ...);
} }
template <class Fn> template <class Fn>
...@@ -146,7 +115,7 @@ private: ...@@ -146,7 +115,7 @@ private:
template <class Fn, size_t... Is> template <class Fn, size_t... Is>
auto fold(Fn&& fn, std::index_sequence<Is...>) { auto fold(Fn&& fn, std::index_sequence<Is...>) {
return fn(at(zipper_index<Is>{})...); return fn(at(zip_index<Is>{})...);
} }
template <class Fn> template <class Fn>
...@@ -158,14 +127,16 @@ private: ...@@ -158,14 +127,16 @@ private:
return fold([](auto&... x) { return std::min({x.buf.size()...}); }); return fold([](auto&... x) { return std::min({x.buf.size()...}); });
} }
// A zipper reached the end if any of its inputs reached the end. // A zip reached the end if any of its inputs reached the end.
bool at_end() { bool at_end() {
return fold([](auto&... inputs) { return (inputs.at_end() || ...); }); return fold([](auto&... inputs) { return (inputs.at_end() || ...); });
} }
// -- callbacks for the forwarders -------------------------------------------
template <size_t I> template <size_t I>
void fwd_on_subscribe(zipper_index<I> index, subscription sub) { void fwd_on_subscribe(zip_index<I> index, subscription sub) {
if (!term_.finalized()) { if (out_) {
auto& in = at(index); auto& in = at(index);
if (!in.sub) { if (!in.sub) {
if (demand_ > 0) if (demand_ > 0)
...@@ -180,10 +151,10 @@ private: ...@@ -180,10 +151,10 @@ private:
} }
template <size_t I> template <size_t I>
void fwd_on_complete(zipper_index<I> index) { void fwd_on_complete(zip_index<I> index) {
if (out_) {
auto& input = at(index); auto& input = at(index);
if (!input.broken) { if (input.sub)
input.broken = true;
input.sub = nullptr; input.sub = nullptr;
if (input.buf.empty()) if (input.buf.empty())
fin(); fin();
...@@ -191,38 +162,39 @@ private: ...@@ -191,38 +162,39 @@ private:
} }
template <size_t I> template <size_t I>
void fwd_on_error(zipper_index<I> index, const error& what) { void fwd_on_error(zip_index<I> index, const error& what) {
if (out_) {
auto& input = at(index); auto& input = at(index);
if (!input.broken) { if (input.sub) {
if (term_.active() && !term_.err()) if (!err_)
term_.err(what); err_ = what;
input.broken = true;
input.sub = nullptr; input.sub = nullptr;
}
if (input.buf.empty()) if (input.buf.empty())
fin(); fin();
} }
} }
template <size_t I, class T> template <size_t I, class T>
void fwd_on_next(zipper_index<I> index, const T& item) { void fwd_on_next(zip_index<I> index, const T& item) {
if (term_.active()) { if (out_) {
at(index).buf.push_back(item); at(index).buf.push_back(item);
push(); push();
} }
} }
private:
void push() { void push() {
if (auto n = std::min(buffered(), demand_); n > 0) { if (auto n = std::min(buffered(), demand_); n > 0) {
for (size_t index = 0; index < n; ++index) { for (size_t index = 0; index < n; ++index) {
fold([this, index](auto&... x) { // fold([this, index](auto&... x) { //
term_.on_next(fn_(x.buf[index]...)); out_.on_next(fn_(x.buf[index]...));
}); });
} }
demand_ -= n; demand_ -= n;
for_each_input([n](auto, auto& x) { // for_each_input([n](auto, auto& x) { //
x.buf.erase(x.buf.begin(), x.buf.begin() + n); x.buf.erase(x.buf.begin(), x.buf.begin() + n);
}); });
term_.push();
} }
if (at_end()) if (at_end())
fin(); fin();
...@@ -230,50 +202,71 @@ private: ...@@ -230,50 +202,71 @@ private:
void fin() { void fin() {
for_each_input([](auto, auto& input) { for_each_input([](auto, auto& input) {
input.in = nullptr;
if (input.sub) { if (input.sub) {
input.sub.cancel(); input.sub.cancel();
input.sub = nullptr; input.sub = nullptr;
} }
input.buf.clear(); input.buf.clear();
}); });
term_.fin(); // Set out_ to null and emit the final event.
auto out = std::move(out_);
if (err_)
out.on_error(err_);
else
out.on_complete();
} }
size_t demand_ = 0; /// Stores the context (coordinator) that runs this flow.
coordinator* ctx_;
/// Reduces n inputs to 1 output.
F fn_; F fn_;
std::tuple<zipper_input<Ts>...> inputs_;
broadcast_step<output_type> term_; /// Stores the required state per input.
std::tuple<zip_input<Ts>...> inputs_;
/// Stores our current demand for items from the subscriber.
size_t demand_ = 0;
/// Stores the first error that occurred on any input.
error err_;
/// Stores a handle to the subscribed observer.
observer<output_type> out_;
}; };
/// Combines items from any number of observables using a zip function.
template <class F, class... Ts> template <class F, class... Ts>
using zipper_impl_ptr = intrusive_ptr<zipper_impl<F, Ts...>>; class zip_with : public cold<zip_with_output_t<F, Ts...>> {
public:
/// @param fn The zip function. Takes one element from each input at a time and // -- member types -----------------------------------------------------------
/// converts them into a single result.
/// @param input0 The input at index 0. using output_type = zip_with_output_t<F, Ts...>;
/// @param input1 The input at index 1.
/// @param inputs The inputs for index > 1. using super = cold<output_type>;
template <class F, class T0, class T1, class... Ts>
auto zip_with(F fn, T0 input0, T1 input1, Ts... inputs) { // -- constructors, destructors, and assignment operators --------------------
using output_type = zipper_output_t<F, typename T0::output_type, //
typename T1::output_type, // zip_with(coordinator* ctx, F fn, observable<Ts>... inputs)
typename Ts::output_type...>; : super(ctx), fn_(std::move(fn)), inputs_(inputs...) {
using impl_t = zipper_impl<F, // // nop
typename T0::output_type, // }
typename T1::output_type, //
typename Ts::output_type...>; // -- implementation of observable<T>::impl ----------------------------------
if (input0.valid() && input1.valid() && (inputs.valid() && ...)) {
auto ctx = input0.ctx(); disposable subscribe(observer<output_type> out) override {
auto ptr = make_counted<impl_t>(ctx, std::move(fn), auto ptr = make_counted<zip_with_sub<F, Ts...>>(super::ctx_, fn_, out,
std::move(input0).as_observable(), inputs_);
std::move(input1).as_observable(), out.on_subscribe(subscription{ptr});
std::move(inputs).as_observable()...); return ptr->as_disposable();
return observable<output_type>{std::move(ptr)};
} else {
return observable<output_type>{};
} }
}
} // namespace caf::flow private:
/// Reduces n inputs to 1 output.
F fn_;
/// Stores the source observables until an observer subscribes.
std::tuple<observable<Ts>...> inputs_;
};
} // namespace caf::flow::op
...@@ -12,7 +12,6 @@ ...@@ -12,7 +12,6 @@
#include "caf/disposable.hpp" #include "caf/disposable.hpp"
#include "caf/error.hpp" #include "caf/error.hpp"
#include "caf/flow/observable.hpp" #include "caf/flow/observable.hpp"
#include "caf/none.hpp"
#include "caf/ref_counted.hpp" #include "caf/ref_counted.hpp"
namespace caf::flow { namespace caf::flow {
...@@ -24,94 +23,7 @@ class single { ...@@ -24,94 +23,7 @@ class single {
public: public:
using output_type = T; using output_type = T;
/// Internal interface of a `single`. explicit single(intrusive_ptr<op::base<T>> pimpl) noexcept
class impl : public observable_impl_base<T> {
public:
using super = observable_impl_base<T>;
CAF_INTRUSIVE_PTR_FRIENDS(impl)
explicit impl(coordinator* ctx) : super(ctx) {
// nop
}
disposable subscribe(observer<T> what) override {
if (!std::holds_alternative<error>(value_)) {
auto res = super::do_subscribe(what.ptr());
observers_.emplace_back(std::move(what), 0u);
return res;
} else {
what.on_error(std::get<error>(value_));
return disposable{};
}
}
void on_request(disposable_impl* sink, size_t n) override {
auto pred = [sink](auto& entry) { return entry.first.ptr() == sink; };
if (auto i = std::find_if(observers_.begin(), observers_.end(), pred);
i != observers_.end()) {
auto f = detail::make_overload( //
[i, n](none_t) { i->second += n; },
[this, i](const T& val) {
i->first.on_next(val);
i->first.on_complete();
observers_.erase(i);
},
[this, i](const error& err) {
i->first.on_error(err);
observers_.erase(i);
});
std::visit(f, value_);
}
}
void on_cancel(disposable_impl* sink) override {
auto pred = [sink](auto& entry) { return entry.first.ptr() == sink; };
if (auto i = std::find_if(observers_.begin(), observers_.end(), pred);
i != observers_.end())
observers_.erase(i);
}
void dispose() override {
if (!std::holds_alternative<error>(value_))
set_error(make_error(sec::disposed));
}
bool disposed() const noexcept override {
return observers_.empty() && !std::holds_alternative<none_t>(value_);
}
void set_value(T val) {
if (std::holds_alternative<none_t>(value_)) {
value_ = std::move(val);
auto& ref = std::get<T>(value_);
auto pred = [](auto& entry) { return entry.second == 0; };
if (auto first = std::partition(observers_.begin(), observers_.end(),
pred);
first != observers_.end()) {
for (auto i = first; i != observers_.end(); ++i) {
i->first.on_next(ref);
i->first.on_complete();
}
observers_.erase(first, observers_.end());
}
}
}
void set_error(error err) {
value_ = std::move(err);
auto& ref = std::get<error>(value_);
for (auto& entry : observers_)
entry.first.on_error(ref);
observers_.clear();
}
private:
std::variant<none_t, T, error> value_;
std::vector<std::pair<observer<T>, size_t>> observers_;
};
explicit single(intrusive_ptr<impl> pimpl) noexcept
: pimpl_(std::move(pimpl)) { : pimpl_(std::move(pimpl)) {
// nop // nop
} }
...@@ -127,13 +39,6 @@ public: ...@@ -127,13 +39,6 @@ public:
single& operator=(single&&) noexcept = default; single& operator=(single&&) noexcept = default;
single& operator=(const single&) noexcept = default; single& operator=(const single&) noexcept = default;
disposable as_disposable() && {
return disposable{std::move(pimpl_)};
}
disposable as_disposable() const& {
return disposable{pimpl_};
}
observable<T> as_observable() && { observable<T> as_observable() && {
return observable<T>{std::move(pimpl_)}; return observable<T>{std::move(pimpl_)};
} }
...@@ -156,16 +61,6 @@ public: ...@@ -156,16 +61,6 @@ public:
std::move(on_error)); std::move(on_error));
} }
void set_value(T val) {
if (pimpl_)
pimpl_->set_value(std::move(val));
}
void set_error(error err) {
if (pimpl_)
pimpl_->set_error(std::move(err));
}
bool valid() const noexcept { bool valid() const noexcept {
return pimpl_ != nullptr; return pimpl_ != nullptr;
} }
...@@ -178,36 +73,23 @@ public: ...@@ -178,36 +73,23 @@ public:
return !valid(); return !valid();
} }
impl* ptr() {
return pimpl_.get();
}
const impl* ptr() const {
return pimpl_.get();
}
const intrusive_ptr<impl>& as_intrusive_ptr() const& noexcept {
return pimpl_;
}
intrusive_ptr<impl>&& as_intrusive_ptr() && noexcept {
return std::move(pimpl_);
}
void swap(single& other) { void swap(single& other) {
pimpl_.swap(other.pimpl_); pimpl_.swap(other.pimpl_);
} }
private: private:
intrusive_ptr<impl> pimpl_; intrusive_ptr<op::base<T>> pimpl_;
}; };
template <class T> /// Convenience function for creating an @ref observable from a concrete
using single_impl = typename single<T>::impl; /// operator type.
template <class Operator, class... Ts>
template <class T> single<typename Operator::output_type>
single<T> make_single(coordinator* ctx) { make_single(coordinator* ctx, Ts&&... xs) {
return single<T>{make_counted<single_impl<T>>(ctx)}; using out_t = typename Operator::output_type;
using ptr_t = intrusive_ptr<out_t>;
ptr_t ptr{new Operator(ctx, std::forward<Ts>(xs)...), false};
return single<out_t>{std::move(ptr)};
} }
} // namespace caf::flow } // namespace caf::flow
...@@ -12,7 +12,9 @@ ...@@ -12,7 +12,9 @@
#include "caf/flow/observer.hpp" #include "caf/flow/observer.hpp"
#include <algorithm> #include <algorithm>
#include <numeric>
#include <type_traits> #include <type_traits>
#include <unordered_set>
namespace caf::flow { namespace caf::flow {
...@@ -107,6 +109,70 @@ struct filter_step { ...@@ -107,6 +109,70 @@ struct filter_step {
} }
}; };
template <class Predicate>
struct take_while_step {
using trait = detail::get_callable_trait_t<Predicate>;
static_assert(std::is_convertible_v<typename trait::result_type, bool>,
"predicates must return a boolean value");
static_assert(trait::num_args == 1,
"predicates must take exactly one argument");
using input_type = std::decay_t<detail::tl_head_t<typename trait::arg_types>>;
using output_type = input_type;
Predicate predicate;
template <class Next, class... Steps>
bool on_next(const input_type& item, Next& next, Steps&... steps) {
if (predicate(item)) {
return next.on_next(item, steps...);
} else {
next.on_complete(steps...);
return false;
}
}
template <class Next, class... Steps>
void on_complete(Next& next, Steps&... steps) {
next.on_complete(steps...);
}
template <class Next, class... Steps>
void on_error(const error& what, Next& next, Steps&... steps) {
next.on_error(what, steps...);
}
};
template <class T>
struct distinct_step {
using input_type = T;
using output_type = T;
std::unordered_set<T> prev;
template <class Next, class... Steps>
bool on_next(const input_type& item, Next& next, Steps&... steps) {
if (prev.insert(item).second)
return next.on_next(item, steps...);
else
return true;
}
template <class Next, class... Steps>
void on_complete(Next& next, Steps&... steps) {
next.on_complete(steps...);
}
template <class Next, class... Steps>
void on_error(const error& what, Next& next, Steps&... steps) {
next.on_error(what, steps...);
}
};
template <class Fn> template <class Fn>
struct map_step { struct map_step {
using trait = detail::get_callable_trait_t<Fn>; using trait = detail::get_callable_trait_t<Fn>;
...@@ -139,6 +205,44 @@ struct map_step { ...@@ -139,6 +205,44 @@ struct map_step {
} }
}; };
template <class T, class Reducer>
struct reduce_step {
T result;
Reducer fn;
reduce_step(T init, Reducer reducer) : result(std::move(init)), fn(reducer) {
// nop
}
reduce_step(reduce_step&&) = default;
reduce_step(const reduce_step&) = default;
reduce_step& operator=(reduce_step&&) = default;
reduce_step& operator=(const reduce_step&) = default;
using input_type = T;
using output_type = T;
template <class Next, class... Steps>
bool on_next(const input_type& item, Next&, Steps&...) {
result = fn(std::move(result), item);
return true;
}
template <class Next, class... Steps>
void on_complete(Next& next, Steps&... steps) {
if (next.on_next(result, steps...))
next.on_complete(steps...);
}
template <class Next, class... Steps>
void on_error(const error& what, Next& next, Steps&... steps) {
if (next.on_next(result, steps...))
next.on_error(what, steps...);
}
};
template <class Fn> template <class Fn>
struct flat_map_optional_step { struct flat_map_optional_step {
using trait = detail::get_callable_trait_t<Fn>; using trait = detail::get_callable_trait_t<Fn>;
...@@ -177,7 +281,7 @@ struct flat_map_optional_step { ...@@ -177,7 +281,7 @@ struct flat_map_optional_step {
}; };
template <class T, class Fn> template <class T, class Fn>
struct do_on_complete_step { struct do_on_next_step {
using input_type = T; using input_type = T;
using output_type = T; using output_type = T;
...@@ -186,12 +290,12 @@ struct do_on_complete_step { ...@@ -186,12 +290,12 @@ struct do_on_complete_step {
template <class Next, class... Steps> template <class Next, class... Steps>
bool on_next(const input_type& item, Next& next, Steps&... steps) { bool on_next(const input_type& item, Next& next, Steps&... steps) {
fn(item);
return next.on_next(item, steps...); return next.on_next(item, steps...);
} }
template <class Next, class... Steps> template <class Next, class... Steps>
void on_complete(Next& next, Steps&... steps) { void on_complete(Next& next, Steps&... steps) {
fn();
next.on_complete(steps...); next.on_complete(steps...);
} }
...@@ -202,7 +306,7 @@ struct do_on_complete_step { ...@@ -202,7 +306,7 @@ struct do_on_complete_step {
}; };
template <class T, class Fn> template <class T, class Fn>
struct do_on_error_step { struct do_on_complete_step {
using input_type = T; using input_type = T;
using output_type = T; using output_type = T;
...@@ -216,18 +320,18 @@ struct do_on_error_step { ...@@ -216,18 +320,18 @@ struct do_on_error_step {
template <class Next, class... Steps> template <class Next, class... Steps>
void on_complete(Next& next, Steps&... steps) { void on_complete(Next& next, Steps&... steps) {
fn();
next.on_complete(steps...); next.on_complete(steps...);
} }
template <class Next, class... Steps> template <class Next, class... Steps>
void on_error(const error& what, Next& next, Steps&... steps) { void on_error(const error& what, Next& next, Steps&... steps) {
fn(what);
next.on_error(what, steps...); next.on_error(what, steps...);
} }
}; };
template <class T, class Fn> template <class T, class Fn>
struct do_finally_step { struct do_on_error_step {
using input_type = T; using input_type = T;
using output_type = T; using output_type = T;
...@@ -241,24 +345,24 @@ struct do_finally_step { ...@@ -241,24 +345,24 @@ struct do_finally_step {
template <class Next, class... Steps> template <class Next, class... Steps>
void on_complete(Next& next, Steps&... steps) { void on_complete(Next& next, Steps&... steps) {
fn();
next.on_complete(steps...); next.on_complete(steps...);
} }
template <class Next, class... Steps> template <class Next, class... Steps>
void on_error(const error& what, Next& next, Steps&... steps) { void on_error(const error& what, Next& next, Steps&... steps) {
fn(); fn(what);
next.on_error(what, steps...); next.on_error(what, steps...);
} }
}; };
/// Catches errors by converting them into complete events instead. template <class T, class Fn>
template <class T> struct do_finally_step {
struct on_error_complete_step {
using input_type = T; using input_type = T;
using output_type = T; using output_type = T;
Fn fn;
template <class Next, class... Steps> template <class Next, class... Steps>
bool on_next(const input_type& item, Next& next, Steps&... steps) { bool on_next(const input_type& item, Next& next, Steps&... steps) {
return next.on_next(item, steps...); return next.on_next(item, steps...);
...@@ -266,331 +370,38 @@ struct on_error_complete_step { ...@@ -266,331 +370,38 @@ struct on_error_complete_step {
template <class Next, class... Steps> template <class Next, class... Steps>
void on_complete(Next& next, Steps&... steps) { void on_complete(Next& next, Steps&... steps) {
fn();
next.on_complete(steps...); next.on_complete(steps...);
} }
template <class Next, class... Steps> template <class Next, class... Steps>
void on_error(const error&, Next& next, Steps&... steps) { void on_error(const error& what, Next& next, Steps&... steps) {
next.on_complete(steps...); fn();
next.on_error(what, steps...);
} }
}; };
/// Wraps logic for pushing data to multiple observers with broadcast semantics, /// Catches errors by converting them into `complete` events instead.
/// i.e., all observers see the same items at the same time and the flow adjusts
/// to the slowest observer. This step may only be used as terminal step.
template <class T> template <class T>
class broadcast_step { struct on_error_complete_step {
public: using input_type = T;
// -- member types -----------------------------------------------------------
using output_type = T; using output_type = T;
using observer_impl_t = observer_impl<output_type>; template <class Next, class... Steps>
bool on_next(const input_type& item, Next& next, Steps&... steps) {
struct output_t { return next.on_next(item, steps...);
size_t demand;
observer<output_type> sink;
};
// -- constructors, destructors, and assignment operators --------------------
broadcast_step() {
// Reserve some buffer space in order to avoid frequent re-allocations while
// warming up.
buf_.reserve(32);
}
// -- properties -------------------------------------------------------------
size_t min_demand() const noexcept {
if (!outputs_.empty()) {
auto i = outputs_.begin();
auto init = (*i++).demand;
return std::accumulate(i, outputs_.end(), init,
[](size_t x, const output_t& y) {
return std::min(x, y.demand);
});
} else {
return 0;
}
}
size_t max_demand() const noexcept {
if (!outputs_.empty()) {
auto i = outputs_.begin();
auto init = (*i++).demand;
return std::accumulate(i, outputs_.end(), init,
[](size_t x, const output_t& y) {
return std::max(x, y.demand);
});
} else {
return 0;
}
}
/// Returns how many items are currently buffered at this step.
size_t buffered() const noexcept {
return buf_.size();
}
/// Returns the number of current observers.
size_t num_observers() const noexcept {
return outputs_.size();
}
/// Convenience function for calling `is_active(state())`;
bool active() const noexcept {
return is_active(state_);
}
/// Queries whether the current state is `observable_state::completing`.
bool completing() const noexcept {
return state_ == observable_state::completing;
}
/// Convenience function for calling `is_final(state())`;
bool finalized() const noexcept {
return is_final(state_);
}
/// Returns the current state.
observable_state state() const noexcept {
return state_;
}
const error& err() const noexcept {
return err_;
}
void err(error x) {
err_ = std::move(x);
}
// -- demand management ------------------------------------------------------
size_t next_demand() {
auto have = buf_.size() + in_flight_;
auto want = max_demand();
if (want > have) {
auto delta = want - have;
in_flight_ += delta;
return delta;
} else {
return 0;
}
}
// -- callbacks for the parent -----------------------------------------------
/// Tries to add a new observer.
bool add(observer<output_type> sink) {
if (is_active(state_)) {
outputs_.emplace_back(output_t{0, std::move(sink)});
return true;
} else if (err_) {
sink.on_error(err_);
return false;
} else {
sink.on_error(make_error(sec::disposed));
return false;
}
}
/// Tries to add a new observer and returns `parent->do_subscribe(sink)` on
/// success or a default-constructed @ref disposable otherwise.
template <class Parent>
disposable add(Parent* parent, observer<output_type> sink) {
if (add(sink)) {
return parent->do_subscribe(sink);
} else {
return disposable{};
}
}
/// Requests `n` more items for `sink`.
/// @returns New demand to signal upstream or 0.
/// @note Calls @ref push.
size_t on_request(disposable_impl* sink, size_t n) {
if (auto i = find(sink); i != outputs_.end()) {
i->demand += n;
push();
return next_demand();
} else {
return 0;
}
}
/// Requests `n` more items for `sink`.
/// @note Calls @ref push and may call `sub.request(n)`.
void on_request(subscription& sub, disposable_impl* sink, size_t n) {
if (auto new_demand = on_request(sink, n); new_demand > 0 && sub)
sub.request(new_demand);
}
/// Removes `sink` from the observer set.
/// @returns New demand to signal upstream or 0.
/// @note Calls @ref push.
size_t on_cancel(disposable_impl* sink) {
if (auto i = find(sink); i != outputs_.end()) {
outputs_.erase(i);
// TODO: shut down on last cancel?
push();
return next_demand();
} else {
return 0;
}
}
/// Requests `n` more items for `sink`.
/// @note Calls @ref push and may call `sub.request(n)`.
void on_cancel(subscription& sub, disposable_impl* sink) {
if (auto new_demand = on_cancel(sink); new_demand > 0 && sub)
sub.request(new_demand);
}
/// Tries to deliver items from the buffer to the observers.
void push() {
// Must not be re-entered. Any on_request call must use the event loop.
CAF_ASSERT(!pushing_);
CAF_DEBUG_STMT(pushing_ = true);
// Sanity checking.
if (outputs_.empty())
return;
// Push data downstream and adjust demand on each path.
if (auto n = std::min(min_demand(), buf_.size()); n > 0) {
auto items = span<output_type>{buf_.data(), n};
for (auto& out : outputs_) {
out.demand -= n;
for (auto& item : items)
out.sink.on_next(item);
}
buf_.erase(buf_.begin(), buf_.begin() + n);
}
if (state_ == observable_state::completing && buf_.empty()) {
if (!err_) {
for (auto& out : outputs_)
out.sink.on_complete();
state_ = observable_state::completed;
} else {
for (auto& out : outputs_)
out.sink.on_error(err_);
state_ = observable_state::aborted;
}
}
CAF_DEBUG_STMT(pushing_ = false);
}
/// Checks whether the broadcaster currently has no pending data.
bool idle() {
return buf_.empty();
}
/// Calls `on_complete` on all observers and drops any pending data.
void close() {
buf_.clear();
if (!err_) {
for (auto& out : outputs_)
out.sink.on_complete();
state_ = observable_state::completed;
} else {
for (auto& out : outputs_)
out.sink.on_error(err_);
state_ = observable_state::aborted;
}
outputs_.clear();
}
/// Calls `on_error` on all observers and drops any pending data.
void abort(const error& reason) {
err_ = reason;
close();
}
// -- callbacks for steps ----------------------------------------------------
bool on_next(const output_type& item) {
// Note: we may receive more data than what we have requested.
if (in_flight_ > 0)
--in_flight_;
buf_.emplace_back(item);
return true;
}
void fin() {
if (is_active(state_)) {
if (idle()) {
close();
} else {
state_ = observable_state::completing;
}
}
}
void on_complete() {
fin();
}
void on_error(const error& what) {
err_ = what;
fin();
}
// -- callbacks for the parent -----------------------------------------------
void dispose() {
on_complete();
}
/// Tries to set the state from `idle` to `running`.
bool start() {
if (state_ == observable_state::idle) {
state_ = observable_state::running;
return true;
} else {
return false;
}
} }
/// Tries to set the state from `idle` to `running`. On success, requests template <class Next, class... Steps>
/// items on `sub` if there is already demand. Calls `sub.cancel()` when void on_complete(Next& next, Steps&... steps) {
/// returning `false`. next.on_complete(steps...);
bool start(subscription& sub) {
if (start()) {
if (auto n = next_demand(); n > 0)
sub.request(n);
return true;
} else {
sub.cancel();
return false;
}
} }
private: template <class Next, class... Steps>
typename std::vector<output_t>::iterator find(disposable_impl* sink) { void on_error(const error&, Next& next, Steps&... steps) {
auto e = outputs_.end(); next.on_complete(steps...);
auto pred = [sink](const output_t& out) { return out.sink.ptr() == sink; };
return std::find_if(outputs_.begin(), e, pred);
} }
/// Buffers outbound items until we can ship them.
std::vector<output_type> buf_;
/// Keeps track of how many items have been requested but did not arrive yet.
size_t in_flight_ = 0;
/// Stores handles to the observer plus their demand.
std::vector<output_t> outputs_;
/// Keeps track of our current state.
observable_state state_ = observable_state::idle;
/// Stores the on_error argument.
error err_;
#ifdef CAF_ENABLE_RUNTIME_CHECKS
/// Protect against re-entering `push`.
bool pushing_ = false;
#endif
}; };
/// Utility for the observables that use one or more steps. /// Utility for the observables that use one or more steps.
......
...@@ -5,7 +5,9 @@ ...@@ -5,7 +5,9 @@
#pragma once #pragma once
#include "caf/detail/core_export.hpp" #include "caf/detail/core_export.hpp"
#include "caf/detail/ref_counted_base.hpp"
#include "caf/disposable.hpp" #include "caf/disposable.hpp"
#include "caf/flow/coordinated.hpp"
#include "caf/flow/fwd.hpp" #include "caf/flow/fwd.hpp"
#include "caf/intrusive_ptr.hpp" #include "caf/intrusive_ptr.hpp"
#include "caf/ref_counted.hpp" #include "caf/ref_counted.hpp"
...@@ -35,55 +37,38 @@ public: ...@@ -35,55 +37,38 @@ public:
void dispose() final; void dispose() final;
}; };
/// A trivial subscription type that drops all member function calls. /// Simple base type for all subscription implementations that implements the
class CAF_CORE_EXPORT nop_impl final : public ref_counted, public impl { /// reference counting member functions.
class CAF_CORE_EXPORT impl_base : public detail::ref_counted_base,
public impl {
public: public:
// -- friends -------------------------------------------------------------- void ref_disposable() const noexcept final;
CAF_INTRUSIVE_PTR_FRIENDS(nop_impl) void deref_disposable() const noexcept final;
bool disposed() const noexcept override;
void ref_disposable() const noexcept override;
void deref_disposable() const noexcept override;
void cancel() override;
void request(size_t n) override;
private:
bool disposed_ = false;
}; };
/// Describes a listener to the subscription that will receive an event /// Describes a listener to the subscription that will receive an event
/// whenever the observer calls `request` or `cancel`. /// whenever the observer calls `request` or `cancel`.
class CAF_CORE_EXPORT listener : public disposable::impl { class CAF_CORE_EXPORT listener : public coordinated {
public: public:
virtual ~listener(); virtual ~listener();
virtual void on_request(disposable::impl* sink, size_t n) = 0; virtual void on_request(coordinated* sink, size_t n) = 0;
virtual void on_cancel(disposable::impl* sink) = 0; virtual void on_cancel(coordinated* sink) = 0;
}; };
/// Default implementation for subscriptions that forward `request` and /// Default implementation for subscriptions that forward `request` and
/// `cancel` to a @ref listener. /// `cancel` to a @ref listener.
class CAF_CORE_EXPORT default_impl final : public ref_counted, public impl { class CAF_CORE_EXPORT fwd_impl final : public impl_base {
public: public:
CAF_INTRUSIVE_PTR_FRIENDS(default_impl) fwd_impl(coordinator* ctx, listener* src, coordinated* snk)
default_impl(coordinator* ctx, listener* src, disposable::impl* snk)
: ctx_(ctx), src_(src), snk_(snk) { : ctx_(ctx), src_(src), snk_(snk) {
// nop // nop
} }
bool disposed() const noexcept override; bool disposed() const noexcept override;
void ref_disposable() const noexcept override;
void deref_disposable() const noexcept override;
void request(size_t n) override; void request(size_t n) override;
void cancel() override; void cancel() override;
...@@ -96,28 +81,28 @@ public: ...@@ -96,28 +81,28 @@ public:
/// @param ctx The owner of @p src and @p snk. /// @param ctx The owner of @p src and @p snk.
/// @param src The @ref observable that emits items. /// @param src The @ref observable that emits items.
/// @param snk the @ref observer that consumes items. /// @param snk the @ref observer that consumes items.
/// @returns an instance of @ref default_impl in a @ref subscription handle. /// @returns an instance of @ref fwd_impl in a @ref subscription handle.
template <class Observable, class Observer> template <class Observable, class Observer>
static subscription make(coordinator* ctx, Observable* src, Observer* snk) { static subscription make(coordinator* ctx, Observable* src, Observer* snk) {
static_assert(std::is_base_of_v<listener, Observable>); static_assert(std::is_base_of_v<listener, Observable>);
static_assert(std::is_base_of_v<disposable_impl, Observer>); static_assert(std::is_base_of_v<coordinated, Observer>);
static_assert(std::is_same_v<typename Observable::output_type, static_assert(std::is_same_v<typename Observable::output_type,
typename Observer::input_type>); typename Observer::input_type>);
intrusive_ptr<impl> ptr{new default_impl(ctx, src, snk), false}; intrusive_ptr<impl> ptr{new fwd_impl(ctx, src, snk), false};
return subscription{std::move(ptr)}; return subscription{std::move(ptr)};
} }
/// Like @ref make but without any type checking. /// Like @ref make but without any type checking.
static subscription make_unsafe(coordinator* ctx, listener* src, static subscription make_unsafe(coordinator* ctx, listener* src,
disposable_impl* snk) { coordinated* snk) {
intrusive_ptr<impl> ptr{new default_impl(ctx, src, snk), false}; intrusive_ptr<impl> ptr{new fwd_impl(ctx, src, snk), false};
return subscription{std::move(ptr)}; return subscription{std::move(ptr)};
} }
private: private:
coordinator* ctx_; coordinator* ctx_;
intrusive_ptr<listener> src_; intrusive_ptr<listener> src_;
intrusive_ptr<disposable_impl> snk_; intrusive_ptr<coordinated> snk_;
}; };
// -- constructors, destructors, and assignment operators -------------------- // -- constructors, destructors, and assignment operators --------------------
...@@ -202,4 +187,7 @@ private: ...@@ -202,4 +187,7 @@ private:
intrusive_ptr<impl> pimpl_; intrusive_ptr<impl> pimpl_;
}; };
/// @ref subscription
using subscription_impl = subscription::impl;
} // namespace caf::flow } // namespace caf::flow
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
#include <cstddef> #include <cstddef>
#include "caf/detail/core_export.hpp" #include "caf/detail/core_export.hpp"
#include "caf/detail/ref_counted_base.hpp"
namespace caf { namespace caf {
...@@ -15,31 +16,13 @@ namespace caf { ...@@ -15,31 +16,13 @@ namespace caf {
/// Serves the requirements of {@link intrusive_ptr}. /// Serves the requirements of {@link intrusive_ptr}.
/// @note *All* instances of `ref_counted` start with a reference count of 1. /// @note *All* instances of `ref_counted` start with a reference count of 1.
/// @relates intrusive_ptr /// @relates intrusive_ptr
class CAF_CORE_EXPORT ref_counted { class CAF_CORE_EXPORT ref_counted : public detail::ref_counted_base {
public: public:
virtual ~ref_counted(); using super = ref_counted_base;
ref_counted(); using super::super;
ref_counted(const ref_counted&);
ref_counted& operator=(const ref_counted&);
/// Increases reference count by one. ~ref_counted() override;
void ref() const noexcept {
rc_.fetch_add(1, std::memory_order_relaxed);
}
/// Decreases reference count by one and calls `request_deletion`
/// when it drops to zero.
void deref() const noexcept;
/// Queries whether there is exactly one reference.
bool unique() const noexcept {
return rc_ == 1;
}
size_t get_reference_count() const noexcept {
return rc_;
}
friend void intrusive_ptr_add_ref(const ref_counted* p) noexcept { friend void intrusive_ptr_add_ref(const ref_counted* p) noexcept {
p->ref(); p->ref();
...@@ -48,9 +31,6 @@ public: ...@@ -48,9 +31,6 @@ public:
friend void intrusive_ptr_release(const ref_counted* p) noexcept { friend void intrusive_ptr_release(const ref_counted* p) noexcept {
p->deref(); p->deref();
} }
protected:
mutable std::atomic<size_t> rc_;
}; };
} // namespace caf } // namespace caf
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
#include "caf/flow/observable.hpp" #include "caf/flow/observable.hpp"
#include "caf/flow/observable_builder.hpp" #include "caf/flow/observable_builder.hpp"
#include "caf/flow/observer.hpp" #include "caf/flow/observer.hpp"
#include "caf/flow/op/cell.hpp"
#include "caf/flow/single.hpp" #include "caf/flow/single.hpp"
#include "caf/scheduled_actor.hpp" #include "caf/scheduled_actor.hpp"
...@@ -26,20 +27,18 @@ namespace caf { ...@@ -26,20 +27,18 @@ namespace caf {
template <class T, class Policy> template <class T, class Policy>
flow::single<T> scheduled_actor::single_from_response_impl(Policy& policy) { flow::single<T> scheduled_actor::single_from_response_impl(Policy& policy) {
using output_type = T; auto cell = make_counted<flow::op::cell<T>>(this);
using impl_type = typename flow::single<output_type>::impl;
auto ptr = make_counted<impl_type>(this);
policy.then( policy.then(
this, this,
[this, ptr](T& val) { [this, cell](T& val) {
ptr->set_value(std::move(val)); cell->set_value(std::move(val));
run_actions(); run_actions();
}, },
[this, ptr](error& err) { [this, cell](error& err) {
ptr->set_error(std::move(err)); cell->set_error(std::move(err));
run_actions(); run_actions();
}); });
return flow::single<output_type>{std::move(ptr)}; return flow::single<T>{std::move(cell)};
} }
} // namespace caf } // namespace caf
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#include "caf/detail/ref_counted_base.hpp"
namespace caf::detail {
ref_counted_base::~ref_counted_base() {
// nop
}
ref_counted_base::ref_counted_base() : rc_(1) {
// nop
}
ref_counted_base::ref_counted_base(const ref_counted_base&) : rc_(1) {
// nop; intentionally don't copy the reference count
}
ref_counted_base& ref_counted_base::operator=(const ref_counted_base&) {
// nop; intentionally don't copy the reference count
return *this;
}
void ref_counted_base::deref() const noexcept {
if (unique() || rc_.fetch_sub(1, std::memory_order_acq_rel) == 1)
delete this;
}
} // namespace caf::detail
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#include "caf/flow/coordinated.hpp"
namespace caf::flow {
coordinated::~coordinated() {
// nop
}
} // namespace caf::flow
...@@ -4,133 +4,4 @@ ...@@ -4,133 +4,4 @@
#include "caf/flow/observable_builder.hpp" #include "caf/flow/observable_builder.hpp"
#include <limits> namespace caf::flow {} // namespace caf::flow
namespace caf::flow {
class interval_action : public ref_counted, public action::impl {
public:
interval_action(intrusive_ptr<interval_impl> impl)
: state_(action::state::scheduled), impl_(std::move(impl)) {
// nop
}
void dispose() override {
state_ = action::state::disposed;
}
bool disposed() const noexcept override {
return state_.load() == action::state::disposed;
}
action::state current_state() const noexcept override {
return state_.load();
}
void run() override {
if (state_.load() == action::state::scheduled)
impl_->fire(this);
}
void ref_disposable() const noexcept override {
ref();
}
void deref_disposable() const noexcept override {
deref();
}
friend void intrusive_ptr_add_ref(const interval_action* ptr) noexcept {
ptr->ref();
}
friend void intrusive_ptr_release(const interval_action* ptr) noexcept {
ptr->deref();
}
private:
std::atomic<action::state> state_;
intrusive_ptr<interval_impl> impl_;
};
interval_impl::interval_impl(coordinator* ctx, timespan initial_delay,
timespan period)
: interval_impl(ctx, initial_delay, period,
std::numeric_limits<int64_t>::max()) {
// nop
}
interval_impl::interval_impl(coordinator* ctx, timespan initial_delay,
timespan period, int64_t max_val)
: super(ctx), initial_delay_(initial_delay), period_(period), max_(max_val) {
CAF_ASSERT(max_val > 0);
}
void interval_impl::dispose() {
if (obs_) {
obs_.on_complete();
obs_ = nullptr;
}
if (pending_) {
pending_.dispose();
pending_ = nullptr;
}
val_ = max_;
}
bool interval_impl::disposed() const noexcept {
return val_ == max_;
}
void interval_impl::on_request(disposable_impl* ptr, size_t n) {
if (obs_.ptr() == ptr) {
if (demand_ == 0 && !pending_) {
if (val_ == 0)
last_ = ctx_->steady_time() + initial_delay_;
else
last_ = ctx_->steady_time() + period_;
pending_ = ctx_->delay_until(last_,
action{make_counted<interval_action>(this)});
}
demand_ += n;
}
}
void interval_impl::on_cancel(disposable_impl* ptr) {
if (obs_.ptr() == ptr) {
obs_ = nullptr;
pending_.dispose();
val_ = max_;
}
}
disposable interval_impl::subscribe(observer<int64_t> sink) {
if (obs_ || val_ == max_) {
sink.on_error(make_error(sec::invalid_observable));
return {};
} else {
obs_ = sink;
return super::do_subscribe(sink.ptr());
}
}
void interval_impl::fire(interval_action* act) {
if (obs_) {
--demand_;
obs_.on_next(val_);
if (++val_ == max_) {
obs_.on_complete();
obs_ = nullptr;
pending_ = nullptr;
} else if (demand_ > 0) {
auto now = ctx_->steady_time();
auto next = last_ + period_;
while (next <= now)
next += period_;
last_ = next;
pending_ = ctx_->delay_until(next, action{act});
}
}
}
} // namespace caf::flow
#include "caf/flow/op/interval.hpp"
#include "caf/flow/subscription.hpp"
#include <limits>
#include <utility>
namespace caf::flow::op {
class interval_action;
class interval_sub_base : public subscription::impl_base {
public:
virtual void fire(interval_action*) = 0;
};
using interval_sub_ptr = intrusive_ptr<interval_sub_base>;
class interval_action : public detail::ref_counted_base, public action::impl {
public:
interval_action(interval_sub_ptr sub)
: state_(action::state::scheduled), sub_(std::move(sub)) {
// nop
}
void dispose() override {
state_ = action::state::disposed;
}
bool disposed() const noexcept override {
return state_.load() == action::state::disposed;
}
action::state current_state() const noexcept override {
return state_.load();
}
void run() override {
if (state_.load() == action::state::scheduled)
sub_->fire(this);
}
void ref_disposable() const noexcept override {
ref();
}
void deref_disposable() const noexcept override {
deref();
}
private:
std::atomic<action::state> state_;
interval_sub_ptr sub_;
};
class interval_sub : public interval_sub_base {
public:
// -- constructors, destructors, and assignment operators --------------------
interval_sub(coordinator* ctx, timespan initial_delay, timespan period,
int64_t max_val, observer<int64_t> out)
: ctx_(ctx),
initial_delay_(initial_delay),
period_(period),
max_(max_val),
out_(std::move(out)) {
CAF_ASSERT(max_val > 0);
}
// -- implementation of subscription_impl ------------------------------------
void cancel() override {
if (out_) {
ctx_->delay_fn([ptr = strong_this()] { ptr->do_cancel(); });
}
}
bool disposed() const noexcept override {
return !out_;
}
void request(size_t n) override {
demand_ += n;
if (!pending_) {
if (val_ == 0)
last_ = ctx_->steady_time() + initial_delay_;
else
last_ = ctx_->steady_time() + period_;
pending_ = ctx_->delay_until(last_,
action{make_counted<interval_action>(this)});
}
}
void fire(interval_action* act) override {
if (out_) {
--demand_;
out_.on_next(val_);
if (++val_ == max_) {
out_.on_complete();
out_ = nullptr;
pending_ = nullptr;
} else if (demand_ > 0) {
auto now = ctx_->steady_time();
auto next = last_ + period_;
while (next <= now)
next += period_;
last_ = next;
pending_ = ctx_->delay_until(next, action{act});
} else {
pending_ = nullptr;
}
}
}
private:
void do_cancel() {
if (out_) {
out_.on_complete();
out_ = nullptr;
}
if (pending_) {
pending_.dispose();
pending_ = nullptr;
}
}
intrusive_ptr<interval_sub> strong_this() {
return intrusive_ptr<interval_sub>{this};
}
coordinator* ctx_;
disposable pending_;
timespan initial_delay_;
timespan period_;
coordinator::steady_time_point last_;
int64_t val_ = 0;
int64_t max_;
size_t demand_ = 0;
observer<int64_t> out_;
};
interval::interval(coordinator* ctx, timespan initial_delay, timespan period)
: interval(ctx, initial_delay, period, std::numeric_limits<int64_t>::max()) {
// nop
}
interval::interval(coordinator* ctx, timespan initial_delay, timespan period,
int64_t max_val)
: super(ctx), initial_delay_(initial_delay), period_(period), max_(max_val) {
// nop
}
disposable interval::subscribe(observer<int64_t> out) {
// Intervals introduce a time dependency, so we need to watch them in order
// to prevent actors from shutting down while timeouts are still pending.
auto ptr = make_counted<interval_sub>(ctx_, initial_delay_, period_, max_,
out);
ctx_->watch(ptr->as_disposable());
out.on_subscribe(subscription{ptr});
return ptr->as_disposable();
}
} // namespace caf::flow::op
...@@ -16,50 +16,30 @@ void subscription::impl::dispose() { ...@@ -16,50 +16,30 @@ void subscription::impl::dispose() {
cancel(); cancel();
} }
bool subscription::nop_impl::disposed() const noexcept { void subscription::impl_base::ref_disposable() const noexcept {
return disposed_; this->ref();
}
void subscription::nop_impl::ref_disposable() const noexcept {
ref();
}
void subscription::nop_impl::deref_disposable() const noexcept {
deref();
}
void subscription::nop_impl::cancel() {
disposed_ = true;
} }
void subscription::nop_impl::request(size_t) { void subscription::impl_base::deref_disposable() const noexcept {
// nop this->deref();
} }
subscription::listener::~listener() { subscription::listener::~listener() {
// nop // nop
} }
bool subscription::default_impl::disposed() const noexcept { bool subscription::fwd_impl::disposed() const noexcept {
return src_ == nullptr; return src_ == nullptr;
} }
void subscription::default_impl::ref_disposable() const noexcept { void subscription::fwd_impl::request(size_t n) {
this->ref();
}
void subscription::default_impl::deref_disposable() const noexcept {
this->deref();
}
void subscription::default_impl::request(size_t n) {
if (src_) if (src_)
ctx()->delay_fn([src = src_, snk = snk_, n] { // ctx()->delay_fn([src = src_, snk = snk_, n] { //
src->on_request(snk.get(), n); src->on_request(snk.get(), n);
}); });
} }
void subscription::default_impl::cancel() { void subscription::fwd_impl::cancel() {
if (src_) { if (src_) {
ctx()->delay_fn([src = src_, snk = snk_] { // ctx()->delay_fn([src = src_, snk = snk_] { //
src->on_cancel(snk.get()); src->on_cancel(snk.get());
......
...@@ -10,22 +10,4 @@ ref_counted::~ref_counted() { ...@@ -10,22 +10,4 @@ ref_counted::~ref_counted() {
// nop // nop
} }
ref_counted::ref_counted() : rc_(1) {
// nop
}
ref_counted::ref_counted(const ref_counted&) : rc_(1) {
// nop; don't copy reference count
}
ref_counted& ref_counted::operator=(const ref_counted&) {
// nop; intentionally don't copy reference count
return *this;
}
void ref_counted::deref() const noexcept {
if (unique() || rc_.fetch_sub(1, std::memory_order_acq_rel) == 1)
delete this;
}
} // namespace caf } // namespace caf
...@@ -28,99 +28,6 @@ struct fixture : test_coordinator_fixture<> { ...@@ -28,99 +28,6 @@ struct fixture : test_coordinator_fixture<> {
BEGIN_FIXTURE_SCOPE(fixture) BEGIN_FIXTURE_SCOPE(fixture)
SCENARIO("concatenate processes inputs sequentially") {
GIVEN("a concatenation with no inputs and shutdown-on-last-complete ON") {
auto uut = make_counted<flow::concat_impl<int>>(ctx.get());
WHEN("subscribing to the concatenation") {
THEN("the concatenation immediately closes") {
auto snk = flow::make_passive_observer<int>();
uut->subscribe(snk->as_observer());
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::aborted);
CHECK_EQ(snk->err, sec::disposed);
CHECK(snk->buf.empty());
}
}
}
GIVEN("a concatenation with no inputs and shutdown-on-last-complete OFF") {
auto uut = make_counted<flow::concat_impl<int>>(ctx.get());
uut->shutdown_on_last_complete(false);
WHEN("subscribing to the concatenation") {
THEN("the concatenation accepts the subscription and does nothing else") {
auto snk = flow::make_passive_observer<int>();
uut->subscribe(snk->as_observer());
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK(snk->buf.empty());
uut->dispose();
ctx->run();
}
}
}
GIVEN("a concatenation with one input that completes") {
WHEN("subscribing and requesting before the first push") {
auto uut = make_counted<flow::concat_impl<int>>(ctx.get());
auto src = flow::make_passive_observable<int>(ctx.get());
uut->add(src->as_observable());
ctx->run();
auto snk = flow::make_passive_observer<int>();
uut->subscribe(snk->as_observer());
ctx->run();
THEN("the concatenation forwards all items from the source") {
MESSAGE("the observer enters the state subscribed");
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls());
MESSAGE("when requesting data, no data is received yet");
snk->sub.request(2);
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls());
MESSAGE("after pushing, the observer immediately receives them");
src->push(1, 2);
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls(1, 2));
MESSAGE("when requesting more data, the observer gets the remainder");
snk->sub.request(20);
ctx->run();
src->push(3, 4, 5);
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5));
MESSAGE("the concatenation closes if the source closes");
src->complete();
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::completed);
CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5));
}
}
}
GIVEN("a concatenation with one input that aborts after some items") {
WHEN("subscribing to the concatenation") {
auto uut = make_counted<flow::concat_impl<int>>(ctx.get());
auto src = flow::make_passive_observable<int>(ctx.get());
uut->add(src->as_observable());
ctx->run();
auto snk = flow::make_passive_observer<int>();
uut->subscribe(snk->as_observer());
ctx->run();
THEN("the concatenation forwards all items until the error") {
MESSAGE("after the source pushed five items, it emits an error");
snk->sub.request(20);
ctx->run();
src->push(1, 2, 3, 4, 5);
ctx->run();
src->abort(make_error(sec::runtime_error));
ctx->run();
MESSAGE("the observer obtains the and then the error");
CHECK_EQ(snk->state, flow::observer_state::aborted);
CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5));
CHECK_EQ(snk->err, make_error(sec::runtime_error));
}
}
}
}
SCENARIO("concat operators combine inputs") { SCENARIO("concat operators combine inputs") {
GIVEN("two observables") { GIVEN("two observables") {
WHEN("merging them to a single publisher via concat") { WHEN("merging them to a single publisher via concat") {
...@@ -128,9 +35,9 @@ SCENARIO("concat operators combine inputs") { ...@@ -128,9 +35,9 @@ SCENARIO("concat operators combine inputs") {
auto outputs = std::vector<int>{}; auto outputs = std::vector<int>{};
auto r1 = ctx->make_observable().repeat(11).take(113); auto r1 = ctx->make_observable().repeat(11).take(113);
auto r2 = ctx->make_observable().repeat(22).take(223); auto r2 = ctx->make_observable().repeat(22).take(223);
flow::concat(std::move(r1), std::move(r2)).for_each([&outputs](int x) { ctx->make_observable()
outputs.emplace_back(x); .concat(std::move(r1), std::move(r2))
}); .for_each([&outputs](int x) { outputs.emplace_back(x); });
ctx->run(); ctx->run();
if (CHECK_EQ(outputs.size(), 336u)) { if (CHECK_EQ(outputs.size(), 336u)) {
CHECK(std::all_of(outputs.begin(), outputs.begin() + 113, CHECK(std::all_of(outputs.begin(), outputs.begin() + 113,
......
...@@ -78,8 +78,8 @@ SCENARIO("concat_map merges multiple observables") { ...@@ -78,8 +78,8 @@ SCENARIO("concat_map merges multiple observables") {
std::iota(inputs.begin(), inputs.end(), 0); std::iota(inputs.begin(), inputs.end(), 0);
self->make_observable() self->make_observable()
.from_container(inputs) .from_container(inputs)
.concat_map([self{self}, add1{adder}](int32_t x) { .concat_map([self = self, adder](int32_t x) {
return self->request(add1, infinite, x).as_observable<int32_t>(); return self->request(adder, infinite, x).as_observable<int32_t>();
}) })
.for_each([&outputs](int32_t x) { outputs.emplace_back(x); }); .for_each([&outputs](int32_t x) { outputs.emplace_back(x); });
launch(); launch();
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// the main distribution directory for license terms and copyright or visit // the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE. // https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#define CAF_SUITE flow.error #define CAF_SUITE flow.fail
#include "caf/flow/observable_builder.hpp" #include "caf/flow/observable_builder.hpp"
...@@ -22,12 +22,12 @@ struct fixture : test_coordinator_fixture<> { ...@@ -22,12 +22,12 @@ struct fixture : test_coordinator_fixture<> {
BEGIN_FIXTURE_SCOPE(fixture) BEGIN_FIXTURE_SCOPE(fixture)
SCENARIO("an error observable immediately calls on_error on any subscriber") { SCENARIO("the fail operator immediately calls on_error on any subscriber") {
GIVEN("an error<int32>") { GIVEN("a fail<int32> operator") {
WHEN("an observer subscribes") { WHEN("an observer subscribes") {
THEN("the observer receives on_error") { THEN("the observer receives on_error") {
auto uut = ctx->make_observable().error<int32_t>(sec::runtime_error); auto uut = ctx->make_observable().fail<int32_t>(sec::runtime_error);
auto snk = flow::make_passive_observer<int32_t>(); auto snk = flow::make_auto_observer<int32_t>();
uut.subscribe(snk->as_observer()); uut.subscribe(snk->as_observer());
ctx->run(); ctx->run();
CHECK(!snk->sub); CHECK(!snk->sub);
......
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#define CAF_SUITE flow.item_publisher
#include "caf/flow/item_publisher.hpp"
#include "core-test.hpp"
#include "caf/flow/observable_builder.hpp"
#include "caf/flow/op/merge.hpp"
#include "caf/flow/scoped_coordinator.hpp"
using namespace caf;
namespace {
struct fixture : test_coordinator_fixture<> {
flow::scoped_coordinator_ptr ctx = flow::make_scoped_coordinator();
template <class... Ts>
std::vector<int> ls(Ts... xs) {
return std::vector<int>{xs...};
}
};
} // namespace
BEGIN_FIXTURE_SCOPE(fixture)
SCENARIO("item publishers discard items that arrive before a subscriber") {
GIVEN("an item publisher") {
WHEN("publishing items") {
THEN("observers see only items that were published after subscribing") {
auto uut = flow::item_publisher<int>{ctx.get()};
uut.push({1, 2, 3});
auto snk = flow::make_auto_observer<int>();
uut.subscribe(snk->as_observer());
ctx->run();
uut.push({4, 5, 6});
ctx->run();
uut.close();
CHECK_EQ(snk->buf, ls(4, 5, 6));
CHECK_EQ(snk->state, flow::observer_state::completed);
}
}
}
}
END_FIXTURE_SCOPE()
...@@ -8,7 +8,9 @@ ...@@ -8,7 +8,9 @@
#include "core-test.hpp" #include "core-test.hpp"
#include "caf/flow/item_publisher.hpp"
#include "caf/flow/observable_builder.hpp" #include "caf/flow/observable_builder.hpp"
#include "caf/flow/op/merge.hpp"
#include "caf/flow/scoped_coordinator.hpp" #include "caf/flow/scoped_coordinator.hpp"
using namespace caf; using namespace caf;
...@@ -22,6 +24,13 @@ struct fixture : test_coordinator_fixture<> { ...@@ -22,6 +24,13 @@ struct fixture : test_coordinator_fixture<> {
std::vector<int> ls(Ts... xs) { std::vector<int> ls(Ts... xs) {
return std::vector<int>{xs...}; return std::vector<int>{xs...};
} }
template <class T>
std::vector<T> concat(std::vector<T> xs, std::vector<T> ys) {
for (auto& y : ys)
xs.push_back(y);
return xs;
}
}; };
} // namespace } // namespace
...@@ -29,40 +38,23 @@ struct fixture : test_coordinator_fixture<> { ...@@ -29,40 +38,23 @@ struct fixture : test_coordinator_fixture<> {
BEGIN_FIXTURE_SCOPE(fixture) BEGIN_FIXTURE_SCOPE(fixture)
SCENARIO("mergers round-robin over their inputs") { SCENARIO("mergers round-robin over their inputs") {
GIVEN("a merger with no inputs and shutdown-on-last-complete ON") { GIVEN("a merger with no inputs") {
auto uut = make_counted<flow::merger_impl<int>>(ctx.get()); auto uut = flow::make_observable<flow::op::merge<int>>(ctx.get());
WHEN("subscribing to the merger") { WHEN("subscribing to the merger") {
THEN("the merger immediately closes") { THEN("the merger immediately closes") {
auto snk = flow::make_passive_observer<int>(); auto snk = flow::make_auto_observer<int>();
uut->subscribe(snk->as_observer()); uut.subscribe(snk->as_observer());
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::aborted);
CHECK_EQ(snk->err, sec::disposed);
CHECK(snk->buf.empty());
}
}
}
GIVEN("a merger with no inputs and shutdown-on-last-complete OFF") {
auto uut = make_counted<flow::merger_impl<int>>(ctx.get());
uut->shutdown_on_last_complete(false);
WHEN("subscribing to the merger") {
THEN("the merger accepts the subscription and does nothing else") {
auto snk = flow::make_passive_observer<int>();
uut->subscribe(snk->as_observer());
ctx->run(); ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed); CHECK_EQ(snk->state, flow::observer_state::completed);
CHECK(snk->buf.empty()); CHECK(snk->buf.empty());
uut->dispose();
ctx->run();
} }
} }
} }
GIVEN("a round-robin merger with one input that completes") { GIVEN("a round-robin merger with one input that completes") {
WHEN("subscribing to the merger and requesting before the first push") { WHEN("subscribing to the merger and requesting before the first push") {
auto uut = make_counted<flow::merger_impl<int>>(ctx.get()); auto src = flow::item_publisher<int>{ctx.get()};
auto src = flow::make_passive_observable<int>(ctx.get()); auto uut = make_counted<flow::op::merge<int>>(ctx.get(),
uut->add(src->as_observable()); src.as_observable());
ctx->run();
auto snk = flow::make_passive_observer<int>(); auto snk = flow::make_passive_observer<int>();
uut->subscribe(snk->as_observer()); uut->subscribe(snk->as_observer());
ctx->run(); ctx->run();
...@@ -76,7 +68,7 @@ SCENARIO("mergers round-robin over their inputs") { ...@@ -76,7 +68,7 @@ SCENARIO("mergers round-robin over their inputs") {
CHECK_EQ(snk->state, flow::observer_state::subscribed); CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls()); CHECK_EQ(snk->buf, ls());
MESSAGE("after pushing, the observer immediately receives them"); MESSAGE("after pushing, the observer immediately receives them");
src->push(1, 2, 3, 4, 5); src.push({1, 2, 3, 4, 5});
ctx->run(); ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed); CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls(1, 2)); CHECK_EQ(snk->buf, ls(1, 2));
...@@ -86,16 +78,16 @@ SCENARIO("mergers round-robin over their inputs") { ...@@ -86,16 +78,16 @@ SCENARIO("mergers round-robin over their inputs") {
CHECK_EQ(snk->state, flow::observer_state::subscribed); CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5)); CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5));
MESSAGE("the merger closes if the source closes"); MESSAGE("the merger closes if the source closes");
src->complete(); src.close();
ctx->run(); ctx->run();
CHECK_EQ(snk->state, flow::observer_state::completed); CHECK_EQ(snk->state, flow::observer_state::completed);
CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5)); CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5));
} }
} }
AND_WHEN("subscribing to the merger pushing before the first request") { AND_WHEN("subscribing to the merger pushing before the first request") {
auto uut = make_counted<flow::merger_impl<int>>(ctx.get()); auto src = flow::item_publisher<int>{ctx.get()};
auto src = flow::make_passive_observable<int>(ctx.get()); auto uut = make_counted<flow::op::merge<int>>(ctx.get(),
uut->add(src->as_observable()); src.as_observable());
ctx->run(); ctx->run();
auto snk = flow::make_passive_observer<int>(); auto snk = flow::make_passive_observer<int>();
uut->subscribe(snk->as_observer()); uut->subscribe(snk->as_observer());
...@@ -105,7 +97,7 @@ SCENARIO("mergers round-robin over their inputs") { ...@@ -105,7 +97,7 @@ SCENARIO("mergers round-robin over their inputs") {
CHECK_EQ(snk->state, flow::observer_state::subscribed); CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls()); CHECK_EQ(snk->buf, ls());
MESSAGE("after pushing, the observer receives nothing yet"); MESSAGE("after pushing, the observer receives nothing yet");
src->push(1, 2, 3, 4, 5); src.push({1, 2, 3, 4, 5});
ctx->run(); ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed); CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls()); CHECK_EQ(snk->buf, ls());
...@@ -120,7 +112,7 @@ SCENARIO("mergers round-robin over their inputs") { ...@@ -120,7 +112,7 @@ SCENARIO("mergers round-robin over their inputs") {
CHECK_EQ(snk->state, flow::observer_state::subscribed); CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5)); CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5));
MESSAGE("the merger closes if the source closes"); MESSAGE("the merger closes if the source closes");
src->complete(); src.close();
ctx->run(); ctx->run();
CHECK_EQ(snk->state, flow::observer_state::completed); CHECK_EQ(snk->state, flow::observer_state::completed);
CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5)); CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5));
...@@ -129,18 +121,17 @@ SCENARIO("mergers round-robin over their inputs") { ...@@ -129,18 +121,17 @@ SCENARIO("mergers round-robin over their inputs") {
} }
GIVEN("a round-robin merger with one input that aborts after some items") { GIVEN("a round-robin merger with one input that aborts after some items") {
WHEN("subscribing to the merger") { WHEN("subscribing to the merger") {
auto uut = make_counted<flow::merger_impl<int>>(ctx.get()); auto src = flow::item_publisher<int>{ctx.get()};
auto src = flow::make_passive_observable<int>(ctx.get()); auto uut = make_counted<flow::op::merge<int>>(ctx.get(),
uut->add(src->as_observable()); src.as_observable());
ctx->run();
auto snk = flow::make_passive_observer<int>(); auto snk = flow::make_passive_observer<int>();
uut->subscribe(snk->as_observer()); uut->subscribe(snk->as_observer());
ctx->run(); ctx->run();
THEN("the merger forwards all items from the source until the error") { THEN("the merger forwards all items from the source until the error") {
MESSAGE("after the source pushed five items, it emits an error"); MESSAGE("after the source pushed five items, it emits an error");
src->push(1, 2, 3, 4, 5); src.push({1, 2, 3, 4, 5});
ctx->run(); ctx->run();
src->abort(make_error(sec::runtime_error)); src.abort(make_error(sec::runtime_error));
ctx->run(); ctx->run();
MESSAGE("when requesting, the observer still obtains the items first"); MESSAGE("when requesting, the observer still obtains the items first");
snk->sub.request(2); snk->sub.request(2);
...@@ -155,61 +146,41 @@ SCENARIO("mergers round-robin over their inputs") { ...@@ -155,61 +146,41 @@ SCENARIO("mergers round-robin over their inputs") {
} }
} }
} }
} GIVEN("a merger that operates on an observable of observables") {
WHEN("subscribing to the merger") {
SCENARIO("merge operators combine inputs") { THEN("the subscribers receives all values from all observables") {
GIVEN("two observables") { auto inputs = std::vector<flow::observable<int>>{
WHEN("merging them to a single publisher") { ctx->make_observable().iota(1).take(3).as_observable(),
THEN("the observer receives the output of both sources") { ctx->make_observable().iota(4).take(3).as_observable(),
auto on_complete_called = false; ctx->make_observable().iota(7).take(3).as_observable(),
auto outputs = std::vector<int>{}; };
auto r1 = ctx->make_observable().repeat(11).take(113); auto snk = flow::make_auto_observer<int>();
auto r2 = ctx->make_observable().repeat(22).take(223); ctx->make_observable()
flow::merge(std::move(r1), std::move(r2)) .from_container(std::move(inputs))
.for_each([&outputs](int x) { outputs.emplace_back(x); }, .merge()
[](const error& err) { FAIL("unexpected error:" << err); }, .subscribe(snk->as_observer());
[&on_complete_called] { on_complete_called = true; });
ctx->run(); ctx->run();
CHECK(on_complete_called); std::sort(snk->buf.begin(), snk->buf.end());
if (CHECK_EQ(outputs.size(), 336u)) { CHECK_EQ(snk->buf, ls(1, 2, 3, 4, 5, 6, 7, 8, 9));
std::sort(outputs.begin(), outputs.end());
CHECK(std::all_of(outputs.begin(), outputs.begin() + 113,
[](int x) { return x == 11; }));
CHECK(std::all_of(outputs.begin() + 113, outputs.end(),
[](int x) { return x == 22; }));
}
} }
} }
} }
} }
SCENARIO("mergers can delay shutdown") { SCENARIO("the merge operator combine inputs") {
GIVEN("a merger with two inputs and shutdown_on_last_complete set to false") { GIVEN("two observables") {
WHEN("both inputs completed") { WHEN("merging them to a single observable") {
THEN("the merger only closes after enabling shutdown_on_last_complete") { THEN("the observer receives the output of both sources") {
auto on_complete_called = false; using ivec = std::vector<int>;
auto outputs = std::vector<int>{}; auto snk = flow::make_auto_observer<int>();
auto merger = make_counted<flow::merger_impl<int>>(ctx.get()); ctx->make_observable()
merger->shutdown_on_last_complete(false); .repeat(11)
merger->add(ctx->make_observable().repeat(11).take(113)); .take(113)
merger->add(ctx->make_observable().repeat(22).take(223)); .merge(ctx->make_observable().repeat(22).take(223))
merger // .subscribe(snk->as_observer());
->as_observable() ctx->run();
.for_each([&outputs](int x) { outputs.emplace_back(x); }, CHECK_EQ(snk->state, flow::observer_state::completed);
[](const error& err) { FAIL("unexpected error:" << err); }, CHECK_EQ(snk->sorted_buf(), concat(ivec(113, 11), ivec(223, 22)));
[&on_complete_called] { on_complete_called = true; });
ctx->run();
CHECK(!on_complete_called);
if (CHECK_EQ(outputs.size(), 336u)) {
std::sort(outputs.begin(), outputs.end());
CHECK(std::all_of(outputs.begin(), outputs.begin() + 113,
[](int x) { return x == 11; }));
CHECK(std::all_of(outputs.begin() + 113, outputs.end(),
[](int x) { return x == 22; }));
}
merger->shutdown_on_last_complete(true);
ctx->run();
CHECK(on_complete_called);
} }
} }
} }
......
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
// Unlike the other test suites, this one does not focus on a single operator.
// Instead, this test suite uses the API to solve some higher level problems to
// exercise a larger chunk of the API all at once.
#define CAF_SUITE flow.mixed
#include "core-test.hpp"
#include "caf/flow/merge.hpp"
#include "caf/flow/observable.hpp"
#include "caf/flow/observable_builder.hpp"
#include "caf/flow/scoped_coordinator.hpp"
using namespace caf;
using caf::flow::make_observer;
namespace {
struct fixture : test_coordinator_fixture<> {
flow::scoped_coordinator_ptr ctx = flow::make_scoped_coordinator();
template <class... Ts>
std::vector<int> ls(Ts... xs) {
return std::vector<int>{xs...};
}
};
} // namespace
#define SUB_CASE(text) \
MESSAGE(text); \
if (true)
BEGIN_FIXTURE_SCOPE(fixture)
SCENARIO("sum up all the multiples of 3 or 5 below 1000") {
SUB_CASE("solution 1") {
auto snk = flow::make_auto_observer<int>();
ctx->make_observable()
.range(1, 999)
.filter([](int x) { return x % 3 == 0 || x % 5 == 0; })
.sum()
.subscribe(snk->as_observer());
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::completed);
CHECK_EQ(snk->buf, ls(233'168));
}
SUB_CASE("solution 2") {
auto snk = flow::make_auto_observer<int>();
ctx->make_observable()
.merge(ctx->make_observable()
.iota(1)
.map([](int x) { return x * 3; })
.take_while([](int x) { return x < 1'000; }),
ctx->make_observable()
.iota(1)
.map([](int x) { return x * 5; })
.take_while([](int x) { return x < 1'000; }))
.distinct()
.sum()
.subscribe(snk->as_observer());
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::completed);
CHECK_EQ(snk->buf, ls(233'168));
}
}
END_FIXTURE_SCOPE()
...@@ -22,20 +22,16 @@ struct fixture : test_coordinator_fixture<> { ...@@ -22,20 +22,16 @@ struct fixture : test_coordinator_fixture<> {
BEGIN_FIXTURE_SCOPE(fixture) BEGIN_FIXTURE_SCOPE(fixture)
SCENARIO("a mute observable never invokes any callbacks except when disposed") { SCENARIO("the never operator never invokes callbacks except when disposed") {
GIVEN("a never<int32>") { GIVEN("a never<int32>") {
WHEN("an observer subscribes") { WHEN("an observer subscribes") {
THEN("the observer never receives any events") { THEN("the observer never receives any events") {
auto uut = ctx->make_observable().never<int32_t>(); auto uut = ctx->make_observable().never<int32_t>();
auto snk = flow::make_passive_observer<int32_t>(); auto snk = flow::make_auto_observer<int32_t>();
uut.subscribe(snk->as_observer()); uut.subscribe(snk->as_observer());
ctx->run(); ctx->run();
if (CHECK(snk->sub)) {
snk->sub.request(42);
ctx->run();
CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK(snk->buf.empty()); CHECK(snk->buf.empty());
} CHECK_EQ(snk->state, flow::observer_state::subscribed);
} }
} }
} }
...@@ -43,21 +39,19 @@ SCENARIO("a mute observable never invokes any callbacks except when disposed") { ...@@ -43,21 +39,19 @@ SCENARIO("a mute observable never invokes any callbacks except when disposed") {
WHEN("an observer subscribes") { WHEN("an observer subscribes") {
THEN("the observer receives on_complete") { THEN("the observer receives on_complete") {
auto uut = ctx->make_observable().never<int32_t>(); auto uut = ctx->make_observable().never<int32_t>();
auto snk1 = flow::make_passive_observer<int32_t>(); auto snk1 = flow::make_auto_observer<int32_t>();
auto snk2 = flow::make_passive_observer<int32_t>(); auto snk2 = flow::make_auto_observer<int32_t>();
uut.subscribe(snk1->as_observer()); auto sub = uut.subscribe(snk1->as_observer());
ctx->run(); ctx->run();
if (CHECK(snk1->sub)) {
snk1->sub.request(42);
ctx->run();
CHECK_EQ(snk1->state, flow::observer_state::subscribed);
CHECK(snk1->buf.empty()); CHECK(snk1->buf.empty());
uut.dispose(); CHECK_EQ(snk1->state, flow::observer_state::subscribed);
sub.dispose();
ctx->run(); ctx->run();
CHECK_EQ(snk1->state, flow::observer_state::completed); CHECK_EQ(snk1->state, flow::observer_state::completed);
MESSAGE("dispose only affects the subscription, "
"the never operator remains unchanged");
uut.subscribe(snk2->as_observer()); uut.subscribe(snk2->as_observer());
CHECK_EQ(snk2->state, flow::observer_state::aborted); CHECK_EQ(snk2->state, flow::observer_state::subscribed);
}
} }
} }
} }
......
...@@ -20,31 +20,15 @@ using namespace caf; ...@@ -20,31 +20,15 @@ using namespace caf;
namespace { namespace {
template <class T>
struct test_observer {
void on_next(T x) {
values.emplace_back(std::move(x));
}
void on_error(const error& what) {
had_error = true;
err = what;
}
void on_complete() {
had_complete = true;
}
std::vector<T> values;
bool had_error = false;
bool had_complete = false;
error err;
};
struct fixture : test_coordinator_fixture<> { struct fixture : test_coordinator_fixture<> {
flow::scoped_coordinator_ptr ctx = flow::make_scoped_coordinator(); flow::scoped_coordinator_ptr ctx = flow::make_scoped_coordinator();
}; };
template <class T, class... Ts>
auto ls(T x, Ts... xs) {
return std::vector<T>{x, xs...};
}
} // namespace } // namespace
BEGIN_FIXTURE_SCOPE(fixture) BEGIN_FIXTURE_SCOPE(fixture)
...@@ -54,84 +38,78 @@ SCENARIO("prefix_and_tail splits off initial elements") { ...@@ -54,84 +38,78 @@ SCENARIO("prefix_and_tail splits off initial elements") {
GIVEN("a generation with 0 values") { GIVEN("a generation with 0 values") {
WHEN("calling prefix_and_tail(2)") { WHEN("calling prefix_and_tail(2)") {
THEN("the observer of prefix_and_tail only receives on_complete") { THEN("the observer of prefix_and_tail only receives on_complete") {
auto inputs = std::vector<int>{}; auto snk = flow::make_auto_observer<tuple_t>();
auto obs = std::make_shared<test_observer<tuple_t>>();
ctx->make_observable() ctx->make_observable()
.from_container(inputs) .empty<int>() //
.prefix_and_tail(2) .prefix_and_tail(2)
.subscribe(flow::make_observer_from_ptr(obs)); .subscribe(snk->as_observer());
ctx->run(); ctx->run();
CHECK(obs->had_complete); CHECK(snk->buf.empty());
CHECK(!obs->had_error); CHECK_EQ(snk->state, flow::observer_state::completed);
CHECK(obs->values.empty()); CHECK_EQ(snk->err, error{});
} }
} }
} }
GIVEN("a generation with 1 values") { GIVEN("a generation with 1 values") {
WHEN("calling prefix_and_tail(2)") { WHEN("calling prefix_and_tail(2)") {
THEN("the observer of prefix_and_tail only receives on_complete") { THEN("the observer of prefix_and_tail only receives on_complete") {
auto inputs = std::vector<int>{1}; auto snk = flow::make_auto_observer<tuple_t>();
auto obs = std::make_shared<test_observer<tuple_t>>();
ctx->make_observable() ctx->make_observable()
.from_container(inputs) .just(1) //
.prefix_and_tail(2) .prefix_and_tail(2)
.subscribe(flow::make_observer_from_ptr(obs)); .subscribe(snk->as_observer());
ctx->run(); ctx->run();
CHECK(obs->had_complete); CHECK(snk->buf.empty());
CHECK(!obs->had_error); CHECK_EQ(snk->state, flow::observer_state::completed);
CHECK(obs->values.empty()); CHECK_EQ(snk->err, error{});
} }
} }
} }
GIVEN("a generation with 2 values") { GIVEN("a generation with 2 values") {
WHEN("calling prefix_and_tail(2)") { WHEN("calling prefix_and_tail(2)") {
THEN("the observer receives the first 2 elements plus empty remainder") { THEN("the observer receives the first 2 elements plus empty remainder") {
auto inputs = std::vector<int>{1, 2}; auto snk = flow::make_auto_observer<int>();
auto prefix_vals = std::vector<int>{1, 2};
auto tail_vals = std::vector<int>{};
auto obs = std::make_shared<test_observer<int>>();
auto flat_map_calls = 0; auto flat_map_calls = 0;
ctx->make_observable() ctx->make_observable()
.from_container(inputs) .iota(1)
.take(2)
.prefix_and_tail(2) .prefix_and_tail(2)
.flat_map([&](const tuple_t& x) { .flat_map([&](const tuple_t& x) {
++flat_map_calls; ++flat_map_calls;
auto& [prefix, tail] = x.data(); auto& [prefix, tail] = x.data();
CHECK_EQ(prefix, prefix_vals); CHECK_EQ(prefix, ls(1, 2));
return tail; return tail;
}) })
.subscribe(flow::make_observer_from_ptr(obs)); .subscribe(snk->as_observer());
ctx->run(); ctx->run();
CHECK(snk->buf.empty());
CHECK_EQ(flat_map_calls, 1); CHECK_EQ(flat_map_calls, 1);
CHECK_EQ(obs->values, tail_vals); CHECK_EQ(snk->state, flow::observer_state::completed);
CHECK(obs->had_complete); CHECK_EQ(snk->err, error{});
CHECK(!obs->had_error);
} }
} }
} }
GIVEN("a generation with 8 values") { GIVEN("a generation with 8 values") {
WHEN("calling prefix_and_tail(2)") { WHEN("calling prefix_and_tail(2)") {
THEN("the observer receives the first 2 elements plus remainder") { THEN("the observer receives the first 2 elements plus remainder") {
auto inputs = std::vector<int>{1, 2, 4, 8, 16, 32, 64, 128}; auto snk = flow::make_auto_observer<int>();
auto prefix_vals = std::vector<int>{1, 2};
auto tail_vals = std::vector<int>{4, 8, 16, 32, 64, 128};
auto obs = std::make_shared<test_observer<int>>();
auto flat_map_calls = 0; auto flat_map_calls = 0;
ctx->make_observable() ctx->make_observable()
.from_container(inputs) .iota(1)
.take(8)
.prefix_and_tail(2) .prefix_and_tail(2)
.flat_map([&](const tuple_t& x) { .flat_map([&](const tuple_t& x) {
++flat_map_calls; ++flat_map_calls;
auto& [prefix, tail] = x.data(); auto& [prefix, tail] = x.data();
CHECK_EQ(prefix, prefix_vals); CHECK_EQ(prefix, ls(1, 2));
return tail; return tail;
}) })
.subscribe(flow::make_observer_from_ptr(obs)); .subscribe(snk->as_observer());
ctx->run(); ctx->run();
CHECK_EQ(flat_map_calls, 1); CHECK_EQ(flat_map_calls, 1);
CHECK_EQ(obs->values, tail_vals); CHECK_EQ(snk->buf, ls(3, 4, 5, 6, 7, 8));
CHECK(obs->had_complete); CHECK_EQ(snk->state, flow::observer_state::completed);
CHECK(!obs->had_error); CHECK_EQ(snk->err, error{});
} }
} }
} }
...@@ -142,69 +120,62 @@ SCENARIO("head_and_tail splits off the first element") { ...@@ -142,69 +120,62 @@ SCENARIO("head_and_tail splits off the first element") {
GIVEN("a generation with 0 values") { GIVEN("a generation with 0 values") {
WHEN("calling head_and_tail") { WHEN("calling head_and_tail") {
THEN("the observer of head_and_tail only receives on_complete") { THEN("the observer of head_and_tail only receives on_complete") {
auto inputs = std::vector<int>{}; auto snk = flow::make_auto_observer<tuple_t>();
auto obs = std::make_shared<test_observer<tuple_t>>(); ctx->make_observable()
ctx // .empty<int>() //
->make_observable()
.from_container(inputs)
.head_and_tail() .head_and_tail()
.subscribe(flow::make_observer_from_ptr(obs)); .subscribe(snk->as_observer());
ctx->run(); ctx->run();
CHECK(obs->had_complete); CHECK(snk->buf.empty());
CHECK(!obs->had_error); CHECK_EQ(snk->state, flow::observer_state::completed);
CHECK(obs->values.empty()); CHECK_EQ(snk->err, error{});
} }
} }
} }
GIVEN("a generation with 1 values") { GIVEN("a generation with 1 values") {
WHEN("calling head_and_tail()") { WHEN("calling head_and_tail()") {
THEN("the observer receives the first element plus empty remainder") { THEN("the observer receives the first element plus empty remainder") {
auto inputs = std::vector<int>{1}; auto snk = flow::make_auto_observer<int>();
auto prefix_val = 1;
auto tail_vals = std::vector<int>{};
auto obs = std::make_shared<test_observer<int>>();
auto flat_map_calls = 0; auto flat_map_calls = 0;
ctx->make_observable() ctx->make_observable()
.from_container(inputs) .just(1)
.head_and_tail() .head_and_tail()
.flat_map([&](const tuple_t& x) { .flat_map([&](const tuple_t& x) {
++flat_map_calls; ++flat_map_calls;
auto& [prefix, tail] = x.data(); auto& [prefix, tail] = x.data();
CHECK_EQ(prefix, prefix_val); CHECK_EQ(prefix, 1);
return tail; return tail;
}) })
.subscribe(flow::make_observer_from_ptr(obs)); .subscribe(snk->as_observer());
ctx->run(); ctx->run();
CHECK(snk->buf.empty());
CHECK_EQ(flat_map_calls, 1); CHECK_EQ(flat_map_calls, 1);
CHECK_EQ(obs->values, tail_vals); CHECK_EQ(snk->state, flow::observer_state::completed);
CHECK(obs->had_complete); CHECK_EQ(snk->err, error{});
CHECK(!obs->had_error);
} }
} }
} }
GIVEN("a generation with 2 values") { GIVEN("a generation with 2 values") {
WHEN("calling head_and_tail()") { WHEN("calling head_and_tail()") {
THEN("the observer receives the first element plus remainder") { THEN("the observer receives the first element plus remainder") {
auto inputs = std::vector<int>{1, 2}; auto snk = flow::make_auto_observer<int>();
auto prefix_val = 1;
auto tail_vals = std::vector<int>{2};
auto obs = std::make_shared<test_observer<int>>();
auto flat_map_calls = 0; auto flat_map_calls = 0;
ctx->make_observable() ctx->make_observable()
.from_container(inputs) .iota(1)
.take(2)
.head_and_tail() .head_and_tail()
.flat_map([&](const tuple_t& x) { .flat_map([&](const tuple_t& x) {
++flat_map_calls; ++flat_map_calls;
auto& [prefix, tail] = x.data(); auto& [prefix, tail] = x.data();
CHECK_EQ(prefix, prefix_val); CHECK_EQ(prefix, 1);
return tail; return tail;
}) })
.subscribe(flow::make_observer_from_ptr(obs)); .subscribe(snk->as_observer());
ctx->run(); ctx->run();
CHECK_EQ(flat_map_calls, 1); CHECK_EQ(flat_map_calls, 1);
CHECK_EQ(obs->values, tail_vals); CHECK_EQ(snk->buf, ls(2));
CHECK(obs->had_complete); CHECK_EQ(snk->state, flow::observer_state::completed);
CHECK(!obs->had_error); CHECK_EQ(snk->err, error{});
} }
} }
} }
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
#include "core-test.hpp" #include "core-test.hpp"
#include "caf/flow/op/cell.hpp"
#include "caf/flow/scoped_coordinator.hpp" #include "caf/flow/scoped_coordinator.hpp"
using namespace caf; using namespace caf;
...@@ -28,21 +29,23 @@ SCENARIO("singles emit at most one value") { ...@@ -28,21 +29,23 @@ SCENARIO("singles emit at most one value") {
WHEN("an observer subscribes before the values has been set") { WHEN("an observer subscribes before the values has been set") {
THEN("the observer receives the value when calling set_value") { THEN("the observer receives the value when calling set_value") {
auto outputs = i32_list{}; auto outputs = i32_list{};
auto single_int = flow::make_single<int32_t>(ctx.get()); auto cell = make_counted<flow::op::cell<int32_t>>(ctx.get());
auto single_int = flow::single<int32_t>{cell};
single_int // single_int //
.as_observable() .as_observable()
.for_each([&outputs](int32_t x) { outputs.emplace_back(x); }); .for_each([&outputs](int32_t x) { outputs.emplace_back(x); });
ctx->run(); ctx->run();
CHECK_EQ(outputs, i32_list()); CHECK_EQ(outputs, i32_list());
single_int.set_value(42); cell->set_value(42);
CHECK_EQ(outputs, i32_list({42})); CHECK_EQ(outputs, i32_list({42}));
} }
} }
WHEN("an observer subscribes after the values has been set") { WHEN("an observer subscribes after the values has been set") {
THEN("the observer receives the value immediately") { THEN("the observer receives the value immediately") {
auto outputs = i32_list{}; auto outputs = i32_list{};
auto single_int = flow::make_single<int32_t>(ctx.get()); auto cell = make_counted<flow::op::cell<int32_t>>(ctx.get());
single_int.set_value(42); auto single_int = flow::single<int32_t>{cell};
cell->set_value(42);
single_int // single_int //
.as_observable() .as_observable()
.for_each([&outputs](int32_t x) { outputs.emplace_back(x); }); .for_each([&outputs](int32_t x) { outputs.emplace_back(x); });
......
...@@ -4,11 +4,10 @@ ...@@ -4,11 +4,10 @@
#define CAF_SUITE flow.zip_with #define CAF_SUITE flow.zip_with
#include "caf/flow/zip_with.hpp" #include "caf/flow/observable_builder.hpp"
#include "core-test.hpp" #include "core-test.hpp"
#include "caf/flow/observable_builder.hpp"
#include "caf/flow/scoped_coordinator.hpp" #include "caf/flow/scoped_coordinator.hpp"
using namespace caf; using namespace caf;
...@@ -27,24 +26,23 @@ SCENARIO("zip_with combines inputs") { ...@@ -27,24 +26,23 @@ SCENARIO("zip_with combines inputs") {
GIVEN("two observables") { GIVEN("two observables") {
WHEN("merging them with zip_with") { WHEN("merging them with zip_with") {
THEN("the observer receives the combined output of both sources") { THEN("the observer receives the combined output of both sources") {
auto o1 = flow::make_passive_observer<int>(); auto snk = flow::make_passive_observer<int>();
auto fn = [](int x, int y) { return x + y; }; ctx->make_observable()
auto r1 = ctx->make_observable().repeat(11).take(113); .zip_with([](int x, int y) { return x + y; },
auto r2 = ctx->make_observable().repeat(22).take(223); ctx->make_observable().repeat(11).take(113),
flow::zip_with(fn, std::move(r1), std::move(r2)) ctx->make_observable().repeat(22).take(223))
.subscribe(o1->as_observer()); .subscribe(snk->as_observer());
ctx->run(); ctx->run();
REQUIRE_EQ(o1->state, flow::observer_state::subscribed); REQUIRE_EQ(snk->state, flow::observer_state::subscribed);
o1->sub.request(64); snk->sub.request(64);
ctx->run(); ctx->run();
CHECK_EQ(o1->state, flow::observer_state::subscribed); CHECK_EQ(snk->state, flow::observer_state::subscribed);
CHECK_EQ(o1->buf.size(), 64u); CHECK_EQ(snk->buf.size(), 64u);
o1->sub.request(64); snk->sub.request(64);
ctx->run(); ctx->run();
CHECK_EQ(o1->state, flow::observer_state::completed); CHECK_EQ(snk->state, flow::observer_state::completed);
CHECK_EQ(o1->buf.size(), 113u); CHECK_EQ(snk->buf.size(), 113u);
CHECK(std::all_of(o1->buf.begin(), o1->buf.begin(), CHECK_EQ(snk->buf, std::vector<int>(113, 33));
[](int x) { return x == 33; }));
} }
} }
} }
...@@ -54,54 +52,16 @@ SCENARIO("zip_with emits nothing when zipping an empty observable") { ...@@ -54,54 +52,16 @@ SCENARIO("zip_with emits nothing when zipping an empty observable") {
GIVEN("two observables, one of them empty") { GIVEN("two observables, one of them empty") {
WHEN("merging them with zip_with") { WHEN("merging them with zip_with") {
THEN("the observer sees on_complete immediately") { THEN("the observer sees on_complete immediately") {
auto o1 = flow::make_passive_observer<int>(); auto snk = flow::make_auto_observer<int>();
auto fn = [](int x, int y, int z) { return x + y + z; }; ctx->make_observable()
auto r1 = ctx->make_observable().repeat(11); .zip_with([](int x, int y, int z) { return x + y + z; },
auto r2 = ctx->make_observable().repeat(22); ctx->make_observable().repeat(11),
auto r3 = ctx->make_observable().empty<int>(); ctx->make_observable().repeat(22),
flow::zip_with(fn, std::move(r1), std::move(r2), std::move(r3)) ctx->make_observable().empty<int>())
.subscribe(o1->as_observer()); .subscribe(snk->as_observer());
ctx->run();
REQUIRE_EQ(o1->state, flow::observer_state::subscribed);
o1->sub.request(64);
ctx->run();
CHECK_EQ(o1->state, flow::observer_state::completed);
CHECK(o1->buf.empty());
}
}
}
}
SCENARIO("zip_with may only have more than one subscriber") {
GIVEN("two observables") {
WHEN("merging them with zip_with") {
THEN("all observer receives the combined output of both sources") {
auto o1 = flow::make_passive_observer<int>();
auto o2 = flow::make_passive_observer<int>();
auto fn = [](int x, int y) { return x + y; };
auto r1 = ctx->make_observable().repeat(11).take(113);
auto r2 = ctx->make_observable().repeat(22).take(223);
auto zip = flow::zip_with(fn, std::move(r1), std::move(r2));
zip.subscribe(o1->as_observer());
zip.subscribe(o2->as_observer());
ctx->run();
REQUIRE_EQ(o1->state, flow::observer_state::subscribed);
o1->sub.request(64);
o2->sub.request(64);
ctx->run();
CHECK_EQ(o1->state, flow::observer_state::subscribed);
CHECK_EQ(o1->buf.size(), 64u);
CHECK_EQ(o2->buf.size(), 64u);
o1->sub.request(64);
o2->dispose();
ctx->run(); ctx->run();
CHECK_EQ(o1->state, flow::observer_state::completed); CHECK(snk->buf.empty());
CHECK_EQ(o1->buf.size(), 113u); CHECK_EQ(snk->state, flow::observer_state::completed);
CHECK(std::all_of(o1->buf.begin(), o1->buf.begin(),
[](int x) { return x == 33; }));
CHECK_EQ(o2->buf.size(), 64u);
CHECK(std::all_of(o2->buf.begin(), o2->buf.begin(),
[](int x) { return x == 33; }));
} }
} }
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment