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

diode.Circuit.scala Maven / Gradle / Ivy

The newest version!
package diode

import scala.annotation.implicitNotFound
import scala.collection.immutable.Queue
import scala.language.higherKinds

/**
  * The `ActionType` type class is used to verify that only valid actions are dispatched. An implicit instance of
  * `ActionType[A]` must be in scope when calling dispatch methods or creating effects that return actions.
  *
  * `ActionType` is contravariant, which means it's enough to have an instance of `ActionType` for a common supertype
  * to be able to dispatch actions of its subtypes. For example providing an instance of `ActionType[Action]` allows
  * dispatching any class that is a subtype of `Action`.
  *
  * @tparam A Action type
  */
@implicitNotFound(msg = "Cannot find an ActionType type class for action of type ${A}. Make sure to provide an implicit ActionType for dispatched actions.")
trait ActionType[-A]

trait Dispatcher {
  def dispatch[A : ActionType](action: A): Unit

  def apply[A : ActionType](action: A) = dispatch(action)
}

/**
  * Base trait for actions. Use this as a basis for your action class hierarchy to get an automatic
  * type class instance of `ActionType[Action]`. Note that this trait is just a helper, you don't need
  * to use it for your actions as you can always define your own `ActionType` instances for your action types.
  */
trait Action

object Action {
  implicit object aType extends ActionType[Action]
}

/**
  * A batch of actions. These actions are dispatched in a batch, without calling listeners in-between the dispatches.
  *
  * @param actions Sequence of actions to dispatch
  */
class ActionBatch private(val actions: Seq[Any]) extends Action {
  def :+[A : ActionType](action: A): ActionBatch =
    new ActionBatch(actions :+ action)

  def +:[A : ActionType](action: A) =
    new ActionBatch(action +: actions)

  def ++(batch: ActionBatch) =
    new ActionBatch(actions ++ batch.actions)
}

object ActionBatch {
  def apply[A : ActionType](actions: A*): ActionBatch = new ActionBatch(actions)
}

/**
  * Use `NoAction` when you need to dispatch an action that does nothing
  */
case object NoAction extends Action

trait ActionProcessor[M <: AnyRef] {
  def process(dispatch: Dispatcher, action: Any, next: Any => ActionResult[M], currentModel: M): ActionResult[M]
}

sealed trait ActionResult[+M] {
  def newModelOpt: Option[M] = None
  def effectOpt: Option[Effect] = None
}

sealed trait ModelUpdated[+M] extends ActionResult[M] {
  def newModel: M
  override def newModelOpt: Option[M] = Some(newModel)
}

sealed trait HasEffect[+M] extends ActionResult[M] {
  def effect: Effect
  override def effectOpt: Option[Effect] = Some(effect)
}

sealed trait UpdateSilent

object ActionResult {

  case object NoChange extends ActionResult[Nothing]

  final case class ModelUpdate[M](newModel: M) extends ModelUpdated[M]

  final case class ModelUpdateSilent[M](newModel: M) extends ModelUpdated[M] with UpdateSilent

  final case class EffectOnly(effect: Effect) extends ActionResult[Nothing] with HasEffect[Nothing]

  final case class ModelUpdateEffect[M](newModel: M, effect: Effect) extends ModelUpdated[M] with HasEffect[M]

  final case class ModelUpdateSilentEffect[M](newModel: M, effect: Effect)
    extends ModelUpdated[M] with HasEffect[M] with UpdateSilent

  def apply[M](model: Option[M], effect: Option[Effect]): ActionResult[M] = (model, effect) match {
    case (Some(m), Some(e)) => ModelUpdateEffect(m, e)
    case (Some(m), None) => ModelUpdate(m)
    case (None, Some(e)) => EffectOnly(e)
    case _ => NoChange
  }
}

trait Circuit[M <: AnyRef] extends Dispatcher {

  type HandlerFunction = (M, Any) => Option[ActionResult[M]]

  private case class Subscription[T](listener: ModelRO[T] => Unit, cursor: ModelR[M, T], lastValue: T) {
    def changed: Option[Subscription[T]] = {
      if (cursor === lastValue)
        None
      else
        Some(copy(lastValue = cursor.eval(model)))
    }

    def call(): Unit = listener(cursor)
  }

  private[diode] var model: M = initialModel

  /**
    * Provides the initial value for the model
    */
  protected def initialModel: M

  /**
    * Handles all dispatched actions
    *
    * @return
    */
  protected def actionHandler: HandlerFunction

  private val modelRW = new RootModelRW[M](model)
  private var isDispatching = false
  private var dispatchQueue = Queue.empty[Any]
  private var listenerId = 0
  private var listeners = Map.empty[Int, Subscription[_]]
  private var processors = List.empty[ActionProcessor[M]]
  private var processChain = buildProcessChain

  private def buildProcessChain = {
    // chain processing functions
    processors.reverse.foldLeft(process _)((next, processor) =>
      (action: Any) => processor.process(this, action, next, model)
    )
  }

  private def baseHandler(action: Any) = action match {
    case batch: ActionBatch =>
      // dispatch all actions in the sequence using internal dispatchBase to prevent
      // additional calls to subscribed listeners
      batch.actions.foreach(a => dispatchBase(a))
      ActionResult.NoChange
    case NoAction =>
      // ignore
      ActionResult.NoChange
    case unknown =>
      handleError(s"Action $unknown was not handled by any action handler")
      ActionResult.NoChange
  }

  /**
    * Zoom into the model using the `get` function
    *
    * @param get Function that returns the part of the model you are interested in
    * @return A `ModelR[T]` giving you read-only access to part of the model
    */
  def zoom[T](get: M => T)(implicit feq: FastEq[_ >: T]): ModelR[M, T] =
    modelRW.zoom[T](get)

  def zoomMap[F[_], A, B](fa: M => F[A])(f: A => B)
    (implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelR[M, F[B]] =
    modelRW.zoomMap(fa)(f)

  def zoomFlatMap[F[_], A, B](fa: M => F[A])(f: A => F[B])
    (implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelR[M, F[B]] =
    modelRW.zoomFlatMap(fa)(f)

  /**
    * Zoom into the model using `get` and `set` functions
    *
    * @param get Function that returns the part of the model you are interested in
    * @param set Function that updates the part of the model you are interested in
    * @return A `ModelRW[T]` giving you read/update access to part of the model
    */
  def zoomRW[T](get: M => T)(set: (M, T) => M)(implicit feq: FastEq[_ >: T]): ModelRW[M, T] = modelRW.zoomRW(get)(set)

  def zoomMapRW[F[_], A, B](fa: M => F[A])(f: A => B)(set: (M, F[B]) => M)
    (implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelRW[M, F[B]] =
    modelRW.zoomMapRW(fa)(f)(set)

  def zoomFlatMapRW[F[_], A, B](fa: M => F[A])(f: A => F[B])(set: (M, F[B]) => M)
    (implicit monad: Monad[F], feq: FastEq[_ >: B]): ModelRW[M, F[B]] =
    modelRW.zoomFlatMapRW(fa)(f)(set)

  /**
    * Subscribes to listen to changes in the model. By providing a `cursor` you can limit
    * what part of the model must change for your listener to be called. If omitted, all changes
    * result in a call.
    *
    * @param cursor   Model reader returning the part of the model you are interested in.
    * @param listener Function to be called when model is updated. The listener function gets
    *                 the model reader as a parameter.
    * @return A function to unsubscribe your listener
    */
  def subscribe[T](cursor: ModelR[M, T])(listener: ModelRO[T] => Unit): () => Unit = {
    this.synchronized {
      listenerId += 1
      val id = listenerId
      listeners += id -> Subscription(listener, cursor, cursor.eval(model))
      () => this.synchronized(listeners -= id)
    }
  }

  /**
    * Adds a new `ActionProcessor[M]` to the action processing chain. The processor is called for
    * every dispatched action.
    *
    * @param processor
    */
  def addProcessor(processor: ActionProcessor[M]): Unit = {
    this.synchronized {
      processors = processor :: processors
      processChain = buildProcessChain
    }
  }

  /**
    * Removes a previously added `ActionProcessor[M]` from the action processing chain.
    *
    * @param processor
    */
  def removeProcessor(processor: ActionProcessor[M]): Unit = {
    this.synchronized {
      processors = processors.filterNot(_ == processor)
      processChain = buildProcessChain
    }
  }

  /**
    * Handle a fatal error. Override this function to do something with exceptions that
    * occur while dispatching actions.
    *
    * @param action Action that caused the exception
    * @param e      Exception that was thrown
    */
  def handleFatal(action: Any, e: Throwable): Unit = throw e

  /**
    * Handle a non-fatal error, such as dispatching an action with no action handler.
    *
    * @param msg Error message
    */
  def handleError(msg: String): Unit = throw new Exception(s"handleError called with: $msg")

  /**
    * Updates the model if it has changed (reference equality check)
    */
  private def update(newModel: M) = {
    if (newModel ne model) {
      model = newModel
    }
  }

  /**
    * The final action processor that does actual action handling.
    *
    * @param action Action to be handled
    * @return
    */
  private def process(action: Any): ActionResult[M] = {
    actionHandler(model, action).getOrElse(baseHandler(action))
  }

  /**
    * Composes multiple handlers into a single handler. Processing stops as soon as a handler is able to handle
    * the action. If none of them handle the action, `None` is returned
    */
  def composeHandlers(handlers: HandlerFunction*): HandlerFunction =
    (model, action) => {
      handlers.foldLeft(Option.empty[ActionResult[M]]) {
        (a, b) => a.orElse(b(model, action))
      }
    }

  @deprecated("Use composeHandlers or foldHandlers instead", "0.5.1")
  def combineHandlers(handlers: HandlerFunction*): HandlerFunction = composeHandlers(handlers: _*)

  /**
    * Folds multiple handlers into a single function so that each handler is called
    * in turn and an updated model is passed on to the next handler. Returned `ActionResult` contains the final model
    * and combined effects.
    */
  def foldHandlers(handlers: HandlerFunction*): HandlerFunction =
    (initialModel, action) => {
      handlers.foldLeft((initialModel, Option.empty[ActionResult[M]])) { case ((currentModel, currentResult), handler) =>
        handler(currentModel, action) match {
          case None =>
            (currentModel, currentResult)
          case Some(result) =>
            val (nextModel, nextResult) = currentResult match {
              case Some(cr) =>
                val newEffect = (cr.effectOpt, result.effectOpt) match {
                  case (Some(e1), Some(e2)) => Some(e1 + e2)
                  case (Some(e1), None) => Some(e1)
                  case (None, Some(e2)) => Some(e2)
                  case (None, None) => None
                }
                val newModel = result.newModelOpt.orElse(cr.newModelOpt)
                (newModel.getOrElse(currentModel), ActionResult(newModel, newEffect))
              case None =>
                (result.newModelOpt.getOrElse(currentModel), result)
            }
            (nextModel, Some(nextResult))
        }
      }._2
    }

  /**
    * Dispatch the action, call change listeners when completed
    *
    * @param action Action to dispatch
    */
  def dispatch[A : ActionType](action: A): Unit = {
    this.synchronized {
      if (!isDispatching) {
        try {
          isDispatching = true
          val oldModel = model
          val silent = dispatchBase(action)
          if (oldModel ne model) {
            // walk through all listeners and update subscriptions when model has changed
            val updated = listeners.foldLeft(listeners) { case (l, (key, sub)) =>
              if (listeners.isDefinedAt(key)) {
                // Listener still exists
                sub.changed match {
                  case Some(newSub) =>
                    // value at the cursor has changed, call listener and update subscription
                    if (!silent) sub.call()
                    l.updated(key, newSub)
                  case None => l // nothing interesting happened
                }
              } else {
                l // Listener was removed since we started
              }
            }

            // Listeners may have changed during processing (subscribe or unsubscribe)
            // so only update the listeners that are still there, and leave any new listeners that may be there now.
            listeners = updated.foldLeft(listeners) { case (l, (key, sub)) =>
              if (l.isDefinedAt(key))
                l.updated(key, sub) // Listener still exists for this key
              else
                l // Listener was removed for this key, skip it
            }
          }
        } catch {
          case e: Throwable =>
            handleFatal(action, e)
        } finally {
          isDispatching = false
        }
        // if there is an item in the queue, dispatch it
        dispatchQueue.dequeueOption foreach { case (nextAction, queue) =>
          dispatchQueue = queue
          dispatch(nextAction)(null)
        }
      } else {
        // add to the queue
        dispatchQueue = dispatchQueue.enqueue(action)
      }
    }
  }

  /**
    * Perform actual dispatching, without calling change listeners
    */
  protected def dispatchBase[A](action: A): Boolean = {
    import AnyAction._
    try {
      processChain(action) match {
        case ActionResult.NoChange =>
          // no-op
          false
        case ActionResult.ModelUpdate(newModel) =>
          update(newModel)
          false
        case ActionResult.ModelUpdateSilent(newModel) =>
          update(newModel)
          true
        case ActionResult.EffectOnly(effects) =>
          // run effects
          effects.run(a => dispatch(a)).recover {
            case e: Throwable =>
              handleError(s"Error in processing effects for action $action: $e")
          }(effects.ec)
          true
        case ActionResult.ModelUpdateEffect(newModel, effects) =>
          update(newModel)
          // run effects
          effects.run(a => dispatch(a)).recover {
            case e: Throwable =>
              handleError(s"Error in processing effects for action $action: $e")
          }(effects.ec)
          false
        case ActionResult.ModelUpdateSilentEffect(newModel, effects) =>
          update(newModel)
          // run effects
          effects.run(a => dispatch(a)).recover {
            case e: Throwable =>
              handleError(s"Error in processing effects for action $action: $e")
          }(effects.ec)
          true
      }
    } catch {
      case e: Throwable =>
        handleFatal(action, e)
        true
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy