Skip to content

Commit

Permalink
New: LazyDerivedVar (#119)
Browse files Browse the repository at this point in the history
  • Loading branch information
raquo committed Jul 10, 2024
1 parent 308231d commit 609e973
Show file tree
Hide file tree
Showing 4 changed files with 1,131 additions and 0 deletions.
53 changes: 53 additions & 0 deletions src/main/scala/com/raquo/airstream/state/LazyDerivedVar.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.raquo.airstream.state

import com.raquo.airstream.core.AirstreamError.VarError
import com.raquo.airstream.core.{AirstreamError, Transaction}

import scala.util.{Failure, Success, Try}

/** LazyDerivedVar has the same Var contract as DerivedVar,
* but it only computes its value lazily, e.g. when you
* ask for it with .now(), or when its signal has subscribers.
*
* Unlike the regular DerivedVar, you don't need to provide an Owner
* to create LazyDerivedVar, and you're allowed to update this Var
* even if its signal has no subscribers.
*/
class LazyDerivedVar[A, B](
parent: Var[A],
zoomIn: A => B,
zoomOut: (A, B) => A,
displayNameSuffix: String
) extends Var[B] {

override private[state] def underlyingVar: SourceVar[_] = parent.underlyingVar

private[this] val _varSignal = new LazyDerivedVarSignal(parent, zoomIn, displayName)

// #Note this getCurrentValue implementation is different from SourceVar
// - SourceVar's getCurrentValue looks at an internal currentValue variable
// - That currentValue gets updated immediately before the signal (in an already existing transaction)
// - I hope this doesn't introduce weird transaction related timing glitches
// - But even if it does, I think keeping derived var's current value consistent with its signal value
// is more important, otherwise it would be madness if the derived var was accessed after its owner
// was killed
override private[state] def getCurrentValue: Try[B] = signal.tryNow()

override private[state] def setCurrentValue(value: Try[B], transaction: Transaction): Unit = {
parent.tryNow() match {
case Success(parentValue) =>
// This can update the parent without causing an infinite loop because
// the parent updates this derived var's signal, it does not call
// setCurrentValue on this var directly.
val nextValue = value.map(zoomOut(parentValue, _))
parent.setCurrentValue(nextValue, transaction)

case Failure(err) =>
AirstreamError.sendUnhandledError(VarError(s"Unable to zoom out of lazy derived var when the parent var is failed.", cause = Some(err)))
}
}

override val signal: StrictSignal[B] = _varSignal

override protected def defaultDisplayName: String = parent.displayName + displayNameSuffix
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.raquo.airstream.state

import com.raquo.airstream.core.Protected
import com.raquo.airstream.misc.MapSignal

import scala.util.Try

class LazyDerivedVarSignal[I, O](
parent: Var[I],
zoomIn: I => O,
parentDisplayName: => String
) extends MapSignal[I, O](parent.signal, project = zoomIn, recover = None) with StrictSignal[O] { self =>

// Note that even if owner kills subscription, this signal might remain due to other listeners
// override protected[state] def isStarted: Boolean = super.isStarted

override protected def defaultDisplayName: String = parentDisplayName + ".signal"

override def tryNow(): Try[O] = {
val newParentLastUpdateId = Protected.lastUpdateId(parent.signal)
if (newParentLastUpdateId != _parentLastUpdateId) {
// This branch can only run if !isStarted
val nextValue = currentValueFromParent()
updateCurrentValueFromParent(nextValue, newParentLastUpdateId)
nextValue
} else {
super.tryNow()
}
}

override protected[state] def updateCurrentValueFromParent(nextValue: Try[O], nextParentLastUpdateId: Int): Unit =
super.updateCurrentValueFromParent(nextValue, nextParentLastUpdateId)
}
18 changes: 18 additions & 0 deletions src/main/scala/com/raquo/airstream/state/Var.scala
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,28 @@ trait Var[A] extends SignalSource[A] with Sink[A] with Named {
}
}

/** Create a strictly evaluated DerivedVar. See also: [[zoomLazy]]. */
def zoom[B](in: A => B)(out: (A, B) => A)(implicit owner: Owner): Var[B] = {
new DerivedVar[A, B](this, in, out, owner, displayNameSuffix = ".zoom")
}

/** Create a lazily evaluated derived Var.
*
* Its value will be evaluated only if it has subscribers,
* or when you get its value with methods like .now(). Its value
* will not be re-evaluated unnecessarily.
*
* Note: if you update a lazy derived Var's value, it is
* not set directly. Instead, you're updating the parent Var,
* and it propagates from there (lazily, in case of zoomLazy).
*
* Note: `in` and `out` functions should be free of side effects,
* as they may not get called if the Var's value is not observed.
*/
def zoomLazy[B](in: A => B)(out: (A, B) => A): Var[B] = {
new LazyDerivedVar[A, B](this, in, out, displayNameSuffix = ".zoomLazy")
}

def setTry(tryValue: Try[A]): Unit = writer.onTry(tryValue)

final def set(value: A): Unit = setTry(Success(value))
Expand Down
Loading

0 comments on commit 609e973

Please sign in to comment.