net.pincette.rs.Merge Maven / Gradle / Ivy
package net.pincette.rs;
import static java.time.Duration.ofNanos;
import static java.util.Arrays.asList;
import static java.util.Arrays.fill;
import static java.util.stream.Collectors.toList;
import static net.pincette.rs.Buffer.buffer;
import static net.pincette.rs.Serializer.dispatch;
import static net.pincette.rs.Util.LOGGER;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Flow.Processor;
import java.util.concurrent.Flow.Publisher;
import java.util.concurrent.Flow.Subscriber;
import java.util.concurrent.Flow.Subscription;
import java.util.function.Predicate;
import java.util.function.Supplier;
/**
* A publisher that emits everything that the given publishers emit.
*
* @param the value type.
* @author Werner Donn\u00e9
* @since 3.0
*/
public class Merge implements Publisher {
private final List branchSubscribers;
private boolean completed;
private long requestSequence;
private Subscriber super T> subscriber;
/**
* Creates a merge publisher.
*
* @param publishers the publishers of which all events are forwarded.
*/
public Merge(final List extends Publisher> publishers) {
branchSubscribers = publishers.stream().map(this::branchSubscriber).collect(toList());
}
/**
* Creates a merge publisher.
*
* @param publishers the publishers of which all events are forwarded.
* @param the value type.
* @return The new publisher.
*/
public static Publisher of(final List extends Publisher> publishers) {
return new Merge<>(publishers);
}
/**
* Creates a merge publisher.
*
* @param publishers the publishers of which all events are forwarded.
* @param the value type.
* @return The new publisher.
*/
@SafeVarargs
public static Publisher of(final Publisher... publishers) {
return new Merge<>(asList(publishers));
}
private boolean allSubscriptions() {
return branchSubscribers.stream().allMatch(s -> s.subscription != null);
}
private BranchSubscriber branchSubscriber(final Publisher publisher) {
final BranchSubscriber s = new BranchSubscriber();
publisher.subscribe(s);
return s;
}
private void notifySubscriber() {
if (subscriber != null && allSubscriptions()) {
subscriber.onSubscribe(new Backpressure());
}
}
public void subscribe(final Subscriber super T> subscriber) {
// The buffer makes sure all branches will be triggered.
final Processor buffer = buffer(branchSubscribers.size(), ofNanos(0));
this.subscriber = buffer;
buffer.subscribe(subscriber);
notifySubscriber();
}
private void trace(final Supplier message) {
LOGGER.finest(() -> getClass().getName() + ": " + message.get());
}
private class Backpressure implements Subscription {
public void cancel() {
branchSubscribers.forEach(b -> b.subscription.cancel());
}
private List behind() {
return selectSubscribers(s -> !s.complete && s.received < s.requested);
}
private List caughtUp() {
return selectSubscribers(s -> !s.complete && s.received == s.requested);
}
public void request(final long n) {
if (n <= 0) {
throw new IllegalArgumentException("A request must be strictly positive.");
}
trace(() -> "request: " + n);
dispatch(
() -> {
if (!completed) {
requestBranches(n);
}
});
}
private void requestBranch(final BranchSubscriber s, final long request) {
trace(() -> "branch request: " + request + " for subscriber " + s);
s.requested += request;
s.subscription.request(request);
s.sequence = ++requestSequence;
}
private void requestBranches(final long request) {
requestCandidates(
Optional.of(caughtUp()).filter(l -> !l.isEmpty()).orElseGet(this::behind), request);
}
private void requestCandidates(final List candidates, final long request) {
if (!candidates.isEmpty()) {
final long[] requests = spreadRequests(request, candidates.size());
for (int i = 0; i < requests.length; ++i) {
if (requests[i] > 0) {
requestBranch(candidates.get(i), requests[i]);
}
}
}
}
private Comparator schedule() {
return Comparator.comparing(s -> s.requested - s.received)
.thenComparing(s -> s.sequence);
}
private List selectSubscribers(final Predicate filter) {
return branchSubscribers.stream().filter(filter).sorted(schedule()).collect(toList());
}
private long[] spreadRequests(final long n, final int numberSubscribers) {
final long[] result = new long[numberSubscribers];
fill(result, 0);
long remaining = n;
while (remaining > 0) {
for (int i = 0; i < result.length && remaining > 0; ++i, --remaining) {
result[i] += 1;
}
}
return result;
}
}
private class BranchSubscriber implements Subscriber {
private boolean complete;
private long received;
private long requested;
private long sequence;
private Subscription subscription;
private boolean allCompleted() {
return branchSubscribers.stream().allMatch(s -> s.complete);
}
private void cancelOthers() {
branchSubscribers.stream()
.filter(s -> s != this)
.map(s -> s.subscription)
.forEach(Subscription::cancel);
}
private void complete() {
complete = true;
if (!completed && allCompleted()) {
completed = true;
trace(() -> "Send onComplete to subscriber " + this);
subscriber.onComplete();
}
}
public void onComplete() {
trace(() -> "onComplete for subscriber " + this);
complete();
}
public void onError(final Throwable throwable) {
subscriber.onError(throwable);
cancelOthers();
}
public void onNext(final T item) {
trace(() -> "Send onNext to subscriber " + this + ": " + item);
++received;
subscriber.onNext(item);
}
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;
notifySubscriber();
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy