Skip to content

Commit

Permalink
WIP: Zooming into a Var should not require an Owner (#119)
Browse files Browse the repository at this point in the history
  • Loading branch information
raquo committed Jun 27, 2024
1 parent 308231d commit 8d881a1
Show file tree
Hide file tree
Showing 4 changed files with 780 additions and 0 deletions.
58 changes: 58 additions & 0 deletions src/main/scala/com/raquo/airstream/state/PureDerivedVar.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.raquo.airstream.state

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

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

// #nc Update comments

/** DerivedVar has the same Var contract as SourceVar, but instead of maintaining its own state
* it is essentially a lens on the underlying SourceVar.
*
* This Var is active for as long as its signal has listeners.
* Being a StrictSignal, it already starts out with a subscription owned by `owner`,
* but even if owner kills its subscriptions, this Var's signal might have other listeners.
*/
class PureDerivedVar[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 PureDerivedVarSignal(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 = {
// #nc Unlike the old DerivedVar, we don't check `_varSignal.isStarted` before updating the parent.
// - Is that "natural" because we don't have an explicit "owner" here, or is that a change in semantics?
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, _))
// println(s">> parent.setCurrentValue($nextValue)")
parent.setCurrentValue(nextValue, transaction)

case Failure(err) =>
AirstreamError.sendUnhandledError(VarError(s"Unable to zoom out of 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, Signal}
import com.raquo.airstream.misc.MapSignal

import scala.util.Try

class PureDerivedVarSignal[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)
}
4 changes: 4 additions & 0 deletions src/main/scala/com/raquo/airstream/state/Var.scala
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ trait Var[A] extends SignalSource[A] with Sink[A] with Named {
new DerivedVar[A, B](this, in, out, owner, displayNameSuffix = ".zoom")
}

def zoomPure[B](in: A => B)(out: (A, B) => A): Var[B] = {
new PureDerivedVar[A, B](this, in, out, displayNameSuffix = ".zoomPure")
}

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 8d881a1

Please sign in to comment.