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

net.pincette.rs.Per Maven / Gradle / Ivy

package net.pincette.rs;

import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;
import static net.pincette.util.ScheduledCompletionStage.runAsyncAfter;
import static net.pincette.util.StreamUtil.generate;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Flow.Processor;
import java.util.concurrent.Flow.Subscription;

/**
 * Buffers a number of values. It always requests the number of values from the publisher that
 * equals the buffer size. It emits the buffered values as a list. This processor uses a shared
 * thread.
 *
 * @param  the value type.
 * @since 2.0
 * @author Werner Donn\u00e8
 */
public class Per extends Buffered> {
  private final Deque buf = new LinkedList<>();
  private final int size;
  private final Duration timeout;
  private boolean touched = true;

  /**
   * Create a buffer of size.
   *
   * @param size the buffer size, which must be larger than zero.
   */
  public Per(final int size) {
    this(size, null);
  }

  /**
   * Create a buffer of size with a timeout.
   *
   * @param size the buffer size, which must be larger than zero.
   * @param timeout the timeout after which the buffer is flushed. It should be positive.
   */
  public Per(final int size, final Duration timeout) {
    this(size, timeout, null);
  }

  /**
   * Create a buffer of size with a timeout.
   *
   * @param size the buffer size, which must be larger than zero.
   * @param timeout the timeout after which the buffer is flushed. It should be positive.
   * @param requestTimeout the time after which an additional element is requested, even if the
   *     upstream publisher hasn't sent all requested elements yet. This provides the opportunity to
   *     the publisher to complete properly when it has fewer elements left than the buffer size. It
   *     may be null.
   * @since 3.0.2
   */
  public Per(final int size, final Duration timeout, final Duration requestTimeout) {
    super(size, requestTimeout);

    if (timeout != null && (timeout.isZero() || timeout.isNegative())) {
      throw new IllegalArgumentException("The timeout should be positive.");
    }

    this.size = size;
    this.timeout = timeout;
  }

  public static  Processor> per(final int size) {
    return new Per<>(size);
  }

  public static  Processor> per(final int size, final Duration timeout) {
    return new Per<>(size, timeout);
  }

  public static  Processor> per(
      final int size, final Duration timeout, final Duration requestTimeout) {
    return new Per<>(size, timeout, requestTimeout);
  }

  private Optional>> consumeBuffer(final boolean flush) {
    return Optional.of(getSlices(flush)).filter(s -> !s.isEmpty());
  }

  private List getSlice(final boolean flush) {
    return buf.size() >= size || (flush && !buf.isEmpty()) ? getSlice() : null;
  }

  private List getSlice() {
    final List result = new ArrayList<>(size);

    for (int i = 0; i < size && !buf.isEmpty(); ++i) {
      result.add(buf.removeLast());
    }

    return result;
  }

  private List> getSlices(final boolean flush) {
    return generate(() -> ofNullable(getSlice(flush))).collect(toList());
  }

  @Override
  protected void last() {
    consumeBuffer(true).ifPresent(this::addValues);
  }

  public boolean onNextAction(final T value) {
    touched = true;
    buf.addFirst(value);
    sendSlices(isCompleted());

    return true;
  }

  private void onNextTimeout() {
    if (!getError() && !buf.isEmpty() && !touched) {
      dispatch(() -> sendSlices(true));
    }

    touched = false;
  }

  @Override
  public void onSubscribe(final Subscription subscription) {
    super.onSubscribe(subscription);

    if (timeout != null) {
      runTimeout();
    }
  }

  private void runTimeout() {
    runAsyncAfter(
        () -> {
          if (!isCompleted()) {
            runTimeout();
            onNextTimeout();
          }
        },
        timeout);
  }

  private void sendSlices(final boolean flush) {
    consumeBuffer(flush)
        .ifPresent(
            list -> {
              addValues(list);
              emit();
            });
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy