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

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