net.pincette.rs.Fanout Maven / Gradle / Ivy
package net.pincette.rs;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Stream.concat;
import static java.util.stream.Stream.empty;
import static net.pincette.rs.Serializer.dispatch;
import static net.pincette.util.StreamUtil.stream;
import static net.pincette.util.StreamUtil.zip;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Flow.Subscriber;
import java.util.concurrent.Flow.Subscription;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;
/**
* A subscriber that accepts multiple subscribers to all of which it passes all events. The pace is
* defined by the slowest subscriber.
*
* @param the value type.
* @author Werner Donn\u00e9
* @since 1.6
*/
public class Fanout implements Subscriber {
private final UnaryOperator duplicator;
private final List subscriptions;
private boolean completed;
private Subscription subscription;
/**
* Creates a fanout subscriber.
*
* @param subscribers the subscribers that all receive the values.
* @param duplicator a function that duplicates the value for all subscribers but the first. This
* way there are as many copies of the value as there are subscribers. It may be null
* .
* @since 3.0
*/
public Fanout(
final List extends Subscriber> subscribers, final UnaryOperator duplicator) {
this.subscriptions = subscribers.stream().map(Backpressure::new).collect(toList());
this.duplicator = duplicator;
}
/**
* Creates a fanout subscriber.
*
* @param subscribers the subscribers that all receive the values.
* @param the value type.
* @return The new subscriber.
*/
public static Subscriber of(final List extends Subscriber> subscribers) {
return new Fanout<>(subscribers, null);
}
/**
* Creates a fanout subscriber.
*
* @param subscribers the subscribers that all receive the values.
* @param duplicator a function that duplicates the value for all subscribers but the first. This
* way there are as many copies of the value as there are subscribers. It may be null
* .
* @param the value type.
* @return The new subscriber.
* @since 3.0
*/
public static Subscriber of(
final List extends Subscriber> subscribers, final UnaryOperator duplicator) {
return new Fanout<>(subscribers, duplicator);
}
/**
* Creates a fanout subscriber.
*
* @param subscribers the subscribers that all receive the values.
* @param the value type.
* @return The new subscriber.
*/
@SafeVarargs
public static Subscriber of(final Subscriber... subscribers) {
return new Fanout<>(asList(subscribers), null);
}
public void onComplete() {
completed = true;
subscriptions.forEach(s -> s.subscriber.onComplete());
}
public void onError(final Throwable t) {
subscriptions.forEach(s -> s.subscriber.onError(t));
}
public void onNext(final T v) {
if (v == null) {
throw new NullPointerException("Can't emit null.");
}
if (!completed) {
zip(subscriptions.stream(), values(v))
.forEach(pair -> pair.first.subscriber.onNext(pair.second));
}
}
public void onSubscribe(final Subscription subscription) {
if (subscription == null) {
throw new NullPointerException("A subscription can't be null.");
}
if (this.subscription != null) {
subscription.cancel();
} else {
this.subscription = subscription;
subscriptions.forEach(s -> s.subscriber.onSubscribe(s));
}
}
private Stream values(final T v) {
final Supplier get = () -> duplicator != null ? duplicator.apply(v) : v;
return concat(
Stream.of(v),
subscriptions.size() > 1
? stream(subscriptions.listIterator(1)).map(s -> get.get())
: empty());
}
private class Backpressure implements Subscription {
private final Subscriber subscriber;
private boolean cancelled;
private long requested;
private Backpressure(final Subscriber subscriber) {
this.subscriber = subscriber;
}
private boolean allCancelled() {
return subscriptions.stream().allMatch(s -> s.cancelled);
}
public void cancel() {
if (!cancelled) {
cancelled = true;
if (allCancelled()) {
subscription.cancel();
}
}
}
private Optional lowestCommon() {
return subscriptions.stream()
.filter(s -> !s.cancelled)
.map(s -> s.requested)
.min((r1, r2) -> (int) (r1 - r2))
.filter(r -> r > 0);
}
public void request(final long number) {
if (number <= 0) {
throw new IllegalArgumentException("A request must be strictly positive.");
}
dispatch(
() -> {
requested += number;
lowestCommon()
.ifPresent(
r -> {
subscriptions.forEach(s -> s.requested -= r);
subscription.request(r);
});
});
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy