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

de.sciss.lucre.IPush.scala Maven / Gradle / Ivy

Go to download

Extension of Scala-STM, adding optional durability layer, and providing API for confluent and reactive event layers

The newest version!
/*
 *  IPush.scala
 *  (Lucre 4)
 *
 *  Copyright (c) 2009-2024 Hanns Holger Rutz. All rights reserved.
 *
 *  This software is published under the GNU Affero General Public License v3+
 *
 *
 *  For further information, please contact Hanns Holger Rutz at
 *  [email protected]
 */

package de.sciss.lucre

import de.sciss.equal.Implicits._
import de.sciss.lucre.IPull.Phase
import de.sciss.lucre.Log.{event => logEvent}
import de.sciss.model.Change

import scala.annotation.elidable
import scala.annotation.elidable.CONFIG
import scala.collection.immutable.{Map => IMap}

object IPush {
  private[lucre] def apply[T <: Exec[T], A](origin: IEvent[T, A], update: A)
                                           (implicit tx: T, targets: ITargets[T]): Unit = {
    val push = new Impl(origin, update)
    logEvent.debug("ipush begin")
    push.visitChildren(origin)
    logEvent.debug("ipull begin")
    push.pull()
    logEvent.debug("ipull end")
  }

  type Parents[T <: Exec[T]] = Set[IEvent[T, Any]]

  private def NoParents[T <: Exec[T]]: Parents[T] = Set.empty[IEvent[T, Any]]

  // private type Visited[S <: Sys[T]] = IMap[Event[T, Any], Parents[T]]
  private final class Reaction[T <: Exec[T], +A](update: A, observers: List[Observer[T, A]]) {
    def apply()(implicit tx: T): Unit =
      observers.foreach(_.apply(update))
  }
  
  private final class Value {
    var state = 0 // 1 = has before, 2 = has now, 4 = has full
    var full  : Option[Any] = None
    var before: Any = _
    var now   : Any = _
    
    def hasBefore : Boolean = (state & 1) != 0
    def hasNow    : Boolean = (state & 2) != 0
    def hasFull   : Boolean = (state & 4) != 0

    def hasPhase(implicit phase: Phase): Boolean =
      if (phase.isNow) hasNow else hasBefore

    def resolvePhase[A](implicit phase: Phase): A =
      if (phase.isNow) now.asInstanceOf[A] else before.asInstanceOf[A]

    def resolveFull[A](implicit phase: Phase): A = {
      val ch = full.get.asInstanceOf[Change[A]]
      if (phase.isNow) ch.now else ch.before
    }

    def setBefore(v: Any): Unit = {
      before = v
      state |= 1
      if (!hasFull && hasNow) createFull()
    }

    def setNow(v: Any): Unit = {
      now = v
      state |= 2
      if (!hasFull && hasBefore) createFull()
    }
    
    def setFull(v: Option[Any]): Unit = {
      full = v
      state |= 4
      if (!hasNow || !hasBefore) v match {
        case Some(Change(_before, _now)) =>
          before  = _before
          now     = _now
          state  |= 3

        case _ =>
      }
    }
    
    def createFull(): Unit = {
      full = if (before == now) None else Some(Change(before, now))
      state |= 4
    }
  }

  private final class Impl[T <: Exec[T]](origin: IEvent[T, Any], val update: Any)
                                        (implicit tx: T, targets: ITargets[T])
    extends IPull[T] {

    private[this] var pushMap   = IMap(origin -> NoParents[T])
    private[this] var pushSeq   = origin :: Nil
    private[this] var pullMap   = IMap.empty[IEvent[T, Any], Value]

    private[this] var indent    = ""

    @elidable(CONFIG) private[this] def incIndent(): Unit = indent += "  "
    @elidable(CONFIG) private[this] def decIndent(): Unit = indent = indent.substring(2)

    private[this] def addVisited(child: IEvent[T, Any], parent: IEvent[T, Any]): Boolean = {
      val parents = pushMap.getOrElse(child, NoParents)
      logEvent.debug(s"${indent}visit $child  (new ? ${parents.isEmpty})")
      val newChild = !pushMap.contains(child)
      if (newChild) pushSeq = child :: pushSeq
      pushMap += ((child, parents + parent))
      parents.isEmpty
    }

    def visitChildren(parent: IEvent[T, Any]): Unit = {
//      val inlet = parent.slot
      incIndent()
      try {
        val childEvents = targets.children(parent) // parent.node._targets.children
        childEvents.foreach { /* case (inlet2, */ child /* ) */ =>
//          if (inlet2 === inlet) {
            visit(child, parent)
//          }
        }

      } finally {
        decIndent()
      }
    }

    def visit(child: IEvent[T, Any], parent: IEvent[T, Any]): Unit =
      if (addVisited(child, parent)) visitChildren(child)

    def contains(source: IEvent[T, Any]): Boolean     = pushMap.contains(source)

    def isOrigin(that  : IEvent[T, Any]): Boolean     = that === origin
    def parents (child : IEvent[T, Any]): Parents[T]  = pushMap.getOrElse(child, NoParents)

    def pull(): Unit = {
      val m0    = currentPull.get()
      val isNew = !m0.contains(tx)
      if (isNew) currentPull.set(m0 + (tx -> this))
      try {
        val reactions: List[Reaction[T, Any]] = pushSeq /*.reverse*/.flatMap { event =>
          val observers: List[Observer[T, Any]] = targets.getEventReactions(event) // tx.reactionMap.getEventReactions(event)
          val hasObs = observers.nonEmpty
          if (hasObs || event.isInstanceOf[Caching]) {
            val opt = apply[Any](event)
            if (hasObs) opt.map(new Reaction(_, observers)) else None
          } else None
        }
        logEvent.debug(s"numReactions = ${reactions.size}")
        reactions.foreach(_.apply())
      } finally {
        if (isNew) currentPull.set(m0)
      }
    }

    def resolve[A]: A = {
      logEvent.debug(s"${indent}resolve")
      update.asInstanceOf[A]
    }

    def resolveChange[A](implicit phase: IPull.Phase): A = {
      val ch = update.asInstanceOf[Change[A]]
      val res = if (phase.isNow) ch.now else ch.before
      logEvent.debug(s"${indent}resolveChange($phase) = $res")
      res
    }

    // this is simply to avoid type mismatch for `A`
    def resolveExpr[A](in: IExpr[T, A])(implicit phase: IPull.Phase): A =
      resolveChange

    // Caches pulled values.
    // Note that we do not check `nonCachedTerms`, implying that
    // `It.Expanded` forbids the use of `pullUpdate` (it throws
    // an exception), and therefore there cannot be a case that
    // circumvents `applyChange` usage.
    def apply[A](source: IEvent[T, A]): Option[A] = {
      incIndent()
      try {
        val resOpt  = pullMap.get(source)
        val isNew   = resOpt.isEmpty
        val res     = if (isNew) new Value else resOpt.get
        logEvent.debug(s"${indent}pull $source  (new ? $isNew; state = ${res.state})")
        if (isNew) pullMap += ((source, res))
        if (res.hasFull) {
          res.full.asInstanceOf[Option[A]]
        } else {
          // XXX TODO --- this is super ugly; a work-around for #68
          // where we would be missing before/now when `pullUpdate` returns `None` because
          // before == now; and then we might be calling `pullUpdate` repeatedly on a `Caching` event
          source match {
            case ce: IChangeEvent[T, A] =>
              if (!res.hasBefore) {
                val vb = ce.pullChange(this)(tx, IPull.Before)
                res.setBefore(vb)
              }
              if (!res.hasNow) {
                val vn = ce.pullChange(this)(tx, IPull.Now)
                res.setNow(vn)
              }
              res.full.asInstanceOf[Option[A]]

            case _ =>
              val v = source.pullUpdate(this)
              res.setFull(v)
              v
          }
        }
      } finally {
        decIndent()
      }
    }

//    def TEST_PURGE_CACHE(): Unit = pullMap = pullMap.empty

    private[this] var nonCachedTerms = List.empty[IEvent[T, Any]]
    private[this] var nonCachedPath  = List.empty[IEvent[T, Any]]

    def nonCached[A](source: IEvent[T, Any])(body: => A): A = {
      val oldTerms  = nonCachedTerms
      val oldPath   = nonCachedPath
      nonCachedTerms  = source :: oldTerms
      nonCachedPath   = Nil
      try {
        body
      } finally {
        nonCachedTerms  = oldTerms
        nonCachedPath   = oldPath
      }
    }

    private def performPullChange[A](source: IChangeEvent[T, A], res: Value)(implicit phase: Phase): A = {
      if (nonCachedTerms.contains(source)) {
        // Console.err.println(s"erasing non-cached path of size ${nonCachedPath.size} for $source")
        // remove cached values in the current call sequence
        pullMap --= nonCachedPath
        source.pullChange(this)
      } else {
        val oldPath = nonCachedPath
        nonCachedPath = source :: oldPath
        val v = try {
          source.pullChange(this)
        } finally {
          nonCachedPath = oldPath
        }
        if (phase.isNow) res.setNow(v) else res.setBefore(v)
        v
      }
    }

    def expr[A](in: IExpr[T, A])(implicit phase: Phase): A = {
      val inCh = in.changed
      if (contains(inCh)) applyChange(inCh)
      else                in.value
    }

    // caches pulled values
    def applyChange[A](source: IChangeEvent[T, A])(implicit phase: Phase): A = {
      incIndent()
      try {
        val resOpt  = pullMap.get(source)
        val res     = resOpt.getOrElse {
          val _res = new Value
          pullMap += ((source, _res))
          _res
        }
        val isOld   = resOpt.isDefined

        logEvent.debug(s"${indent}pull $source  (old ? $isOld; state = ${res.state})")
        if      (isOld && res.hasPhase      ) res.resolvePhase[A]
        else if (isOld && res.full.isDefined) res.resolveFull [A]
        else {
          performPullChange(source, res)
        }
      } finally {
        decIndent()
      }
    }
  }

  private val currentPull = new ThreadLocal[IMap[Any, IPull[_]]]() {
    override def initialValue(): IMap[Any, IPull[_]] = IMap.empty
  }

  def tryPull[T <: Exec[T], A](source: IEvent[T, A])(implicit tx: T): Option[A] =
    currentPull.get().get(tx).flatMap { p =>
      val pc = p.asInstanceOf[IPull[T]]
      if (pc.contains(source)) pc(source) else None
    }
}

object IPull {
  sealed trait Phase {
    def isBefore: Boolean
    def isNow   : Boolean
  }
  case object Before extends Phase { def isBefore = true  ; def isNow = false }
  case object Now    extends Phase { def isBefore = false ; def isNow = true  }
}
trait IPull[T <: Exec[T]] {
  /** Assuming that the caller is origin of the event, resolves the update of the given type. */
  def resolve[A]: A

  def resolveChange[A](implicit phase: IPull.Phase): A

  def resolveExpr[A](in: IExpr[T, A])(implicit phase: IPull.Phase): A

  /** Retrieves the immediate parents from the push phase. */
  def parents(source: IEvent[T, Any]): IPush.Parents[T]

  /** Pulls the update from the given source. */
  def apply[A](source: IEvent[T, A]): Option[A]

  def applyChange[A](source: IChangeEvent[T, A])(implicit phase : Phase): A

  /** Pulls the value from the given expression. If `in` is
    * part of the event graph, pulls the update, otherwise returns the current value. */
  def expr[A](in: IExpr[T, A])(implicit phase : Phase): A

  /** Whether the selector has been visited during the push phase. */
  def contains(source: IEvent[T, Any]): Boolean

  def isOrigin(source: IEvent[T, Any]): Boolean

  /** Marks a region of the pull action as non-caching.
    * This is done by submitting a terminal symbol `source`, typically
    * an instance of `It.Expanded`. When anything within the `body` tries to apply
    * the `source`, all values from events on the call tree will be removed
    * from cache.
    */
  def nonCached[A](source: IEvent[T, Any])(body: => A): A

//  def TEST_PURGE_CACHE(): Unit
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy