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

monifu.reactive.observers.CacheUntilConnectSubscriber.scala Maven / Gradle / Ivy

/*
 * Copyright (c) 2014-2016 by its authors. Some rights reserved.
 * See the project homepage at: https://monix.io
 *
 * 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 monifu.reactive.observers

import monifu.reactive.Ack.{Cancel, Continue}
import monifu.reactive._
import monifu.reactive.internals._

import scala.collection.mutable
import scala.concurrent.{Future, Promise}

/**
 * Wraps an `underlying` [[Subscriber]] into an implementation that caches
 * all events until the call to `connect()` happens. After being connected,
 * the buffer is drained into the `underlying` observer, after which all
 * subsequent events are pushed directly.
 */
final class CacheUntilConnectSubscriber[-T] private (downstream: Subscriber[T])
  extends Subscriber[T] { self =>

  val scheduler = downstream.scheduler
  private[this] implicit val s = scheduler
  private[this] val lock = new AnyRef

  // MUST BE synchronized by `lock`, only available if isConnected == false
  private[this] var queue = mutable.ArrayBuffer.empty[T]
  // MUST BE synchronized by `lock`
  private[this] var isConnectionStarted = false
  // MUST BE synchronized by `lock`, as long as isConnected == false
  private[this] var wasCanceled = false

  // Promise guaranteed to be fulfilled once isConnected is
  // seen as true and used for back-pressure.
  // MUST BE synchronized by `lock`, only available if isConnected == false
  private[this] val connectedPromise = Promise[Ack]()
  private[this] var connectedFuture = connectedPromise.future

  // Volatile that is set to true once the buffer is drained.
  // Once visible as true, it implies that the queue is empty
  // and has been drained and thus the onNext/onError/onComplete
  // can take the fast path
  @volatile private[this] var isConnected = false

  /**
   * Connects the underling observer to the upstream publisher.
   *
   * This function should be idempotent. Calling it multiple times should have the same
   * effect as calling it once.
   */
  def connect(): Unit = lock.synchronized {
    if (!isConnected && !isConnectionStarted) {
      isConnectionStarted = true

      Observable.fromIterable(queue).onSubscribe(new Observer[T] {
        private[this] val bufferWasDrained = Promise[Ack]()

        bufferWasDrained.future.onSuccess {
          case Continue =>
            connectedPromise.success(Continue)
            isConnected = true
            queue = null // gc relief

          case Cancel =>
            wasCanceled = true
            connectedPromise.success(Cancel)
            isConnected = true
            queue = null // gc relief
        }

        def onNext(elem: T): Future[Ack] = {
          downstream.onNext(elem)
            .ifCancelTryCanceling(bufferWasDrained)
        }

        def onComplete(): Unit = {
          bufferWasDrained.trySuccess(Continue)
        }

        def onError(ex: Throwable): Unit = {
          if (bufferWasDrained.trySuccess(Continue))
            self.onError(ex)
          else
            s.reportFailure(ex)
        }
      })
    }
  }

  def onNext(elem: T) = {
    if (!isConnected) lock.synchronized {
      // checking again because of multi-threading concerns
      if (!isConnected && !isConnectionStarted) {
        // we can cache the incoming event
        queue.append(elem)
        Continue
      }
      else {
        // if the connection started, we cannot modify the queue anymore
        // so we must be patient and apply back-pressure
        connectedFuture = connectedFuture.flatMap {
          case Cancel => Cancel
          case Continue =>
            downstream.onNext(elem)
        }

        connectedFuture
      }
    }
    else if (!wasCanceled) {
      // taking fast path :-)
      downstream.onNext(elem)
    }
    else {
      // was canceled either during connect, or the upstream publisher
      // sent an onNext event after onComplete / onError
      Cancel
    }
  }

  def onComplete() = {
    // we cannot take a fast path here
    connectedFuture.onContinueSignalComplete(downstream)
  }

  def onError(ex: Throwable) = {
    // we cannot take a fast path here
    connectedFuture.onContinueSignalError(downstream, ex)
  }
}

object CacheUntilConnectSubscriber {
  /** Builder for [[CacheUntilConnectSubscriber]] */
  def apply[T](underlying: Subscriber[T]): CacheUntilConnectSubscriber[T] =
    new CacheUntilConnectSubscriber(underlying)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy