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

korolev.effect.Hub.scala Maven / Gradle / Ivy

/*
 * Copyright 2017-2020 Aleksey Fomkin
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package korolev.effect

import java.util.concurrent.atomic.AtomicBoolean

import korolev.effect.syntax._

import scala.collection.concurrent.TrieMap

/**
  * A function which returns new streams which
  * contains same elements as the parent stream.
  * This is helpful when you want to consume
  * content of the stream in few different places.
  */
final class Hub[F[_]: Effect, T](upstream: Stream[F, T], bufferSize: Int) {

  @volatile private var closed = false

  private val queues = TrieMap.empty[Queue[F, T], Unit]
  private val inProgress = new AtomicBoolean(false)

  private final class StreamOnePullAtTime(thisQueue: Queue[F, T]) extends Stream[F, T] {

    private def begin(): F[Boolean] =
      Effect[F].delay(inProgress.compareAndSet(false, true))

    private def end(): F[Unit] =
      Effect[F].delay {
        val end = inProgress.compareAndSet(true, false)
        if (end == false) throw new IllegalStateException("Can't be false")
      }

    private def pullUpstream(): F[Option[T]] = upstream
      .pull()
      .map { maybeItem =>
        maybeItem match {
          case Some(item) =>
            queues.keysIterator.foreach { queue =>
              if (queue != thisQueue)
                queue.offerUnsafe(item)
            }
          case None =>
            closed = true
            queues.keysIterator.foreach(_.unsafeClose())
        }
        maybeItem
      }

    def pull(): F[Option[T]] =
      for {
        began <- begin()
        result <- if (began)
          for {
            size <- thisQueue.size()
            result <- if (size > 0) end() *> thisQueue.stream.pull()
            else pullUpstream().flatMap(v => end().as(v))
          } yield result
        else thisQueue.stream.pull()
      } yield result

    def cancel(): F[Unit] = thisQueue.close()
  }

  private final class QueueRemoveFromHubOnClose extends Queue[F, T](bufferSize) {
    override def unsafeClose(): Unit = {
      queues.remove(this)
      super.unsafeClose()
    }
  }

  def newStreamUnsafe(): Stream[F, T] = {
    if (closed)
      throw new IllegalStateException("Hub is closed")
    val queue = new QueueRemoveFromHubOnClose()
    val stream = new StreamOnePullAtTime(queue)
    queues.put(queue, ())
    stream
  }

  def newStream(): F[Stream[F, T]] = Effect[F].delay {
    newStreamUnsafe()
  }
}

object Hub {

  /**
    * @see Hub
    */
  def apply[F[_]: Effect, T](stream: Stream[F, T], bufferSize: Int = Int.MaxValue): Hub[F, T] =
    new Hub(stream, bufferSize)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy