All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 subscriber;

  /**
   * Creates a merge publisher.
   *
   * @param publishers the publishers of which all events are forwarded.
   */
  public Merge(final List> 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> 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 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