Skip to content

Commit

Permalink
New: StrictSignal.mapLazy
Browse files Browse the repository at this point in the history
  • Loading branch information
raquo committed Dec 12, 2024
1 parent 7d89145 commit 7b01cbe
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 0 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ Note: `withCurrentValueOf` and `sample` operators are also available on signals,

If you want to get a Signal's current value without the complications of sampling, or even if you just want to make sure that a Signal is started, just call `observe` on it. That will add a noop observer to the signal, and return an `OwnedSignal` instance which being a `StrictSignal`, does expose `now()` and `tryNow()` methods that safely provide you with its current value. However, you will need to provide an `Owner` to do that. More on those later.

If you already have a `StrictSignal`, you can map over it with `mapLazy` to get another `StrictSignal` (#TODO: Move this doc elsewhere).


### Relationship between EventStream and Signal

Expand Down Expand Up @@ -703,6 +705,8 @@ As the name implies, `LazyDerivedVar` is evaluated lazily, unlike other Vars. Th

Before the introduction of `zoomLazy`, Airstream also offered a strict `zoom` method, which is now considered inferior, because it requires an `Owner`. Note that derived vars created with the old `zoom` method could only be updated if their owner remained active, or if they had any other subscribers. Otherwise, attempting to update the var would cause Airstream to emit an unhandled error. The old `zoom` method will be deprecated in 18.0.0.

The Signal-only companion to `zoomLazy` is `mapLazy`, available on any `StrictSignal`.

// TODO[18.0.0] - Reorganize this section, split out for every operator.

##### Var.bimap
Expand Down
15 changes: 15 additions & 0 deletions src/main/scala/com/raquo/airstream/state/StrictSignal.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ import scala.util.Try
*/
trait StrictSignal[+A] extends Signal[A] {

/** Map StrictSignal to get another StrictSignal, without requiring an Owner.
*
* The mechanism is similar to Var.zoomLazy.
*
* Just as `zoomLazy`, this method will be renamed in the next major Airstream release.
*/
def mapLazy[B](project: A => B): StrictSignal[B] = {
new LazyStrictSignal(
parentSignal = this,
zoomIn = project,
parentDisplayName = displayName,
displayNameSuffix = ".mapLazy"
)
}

/** Get the signal's current value
*
* @throws Throwable the error from the current value's Failure
Expand Down
158 changes: 158 additions & 0 deletions src/test/scala/com/raquo/airstream/state/StrictSignalSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package com.raquo.airstream.state

import com.raquo.airstream.UnitSpec
import com.raquo.airstream.core.AirstreamError
import com.raquo.airstream.fixtures.Effect
import org.scalatest.BeforeAndAfter

import scala.collection.mutable
import scala.util.{Failure, Success}

class StrictSignalSpec extends UnitSpec with BeforeAndAfter {

case class Form(int: Int)

case class MetaForm(form: Form)

private val errorEffects = mutable.Buffer[Effect[Throwable]]()

private val errorCallback = (err: Throwable) => {
errorEffects += Effect("unhandled", err)
()
}

before {
errorEffects.clear()
AirstreamError.registerUnhandledErrorCallback(errorCallback)
AirstreamError.unregisterUnhandledErrorCallback(AirstreamError.consoleErrorCallback)
}

after {
AirstreamError.registerUnhandledErrorCallback(AirstreamError.consoleErrorCallback)
AirstreamError.unregisterUnhandledErrorCallback(errorCallback)
assert(errorEffects.isEmpty) // #Note this fails the test rather inelegantly
}

it("LazyStrictSignal: lazy eval") {

val effects: mutable.Buffer[String] = mutable.Buffer()

val s = Var(Form(10))
val d = s.signal.mapLazy[Int] { form =>
effects += s"project-${form.int}"
form.int
}

assert(effects.toList == Nil)

// --

assert(s.tryNow() == Success(Form(10)))
assert(s.now() == Form(10))
assert(s.signal.now() == Form(10))

assert(effects.toList == Nil)

assert(d.tryNow() == Success(10))
assert(d.now() == 10)

assert(effects.toList == List(
"project-10"
))
effects.clear()

// --

s.writer.onNext(Form(30))

assert(s.tryNow() == Success(Form(30)))
assert(s.now() == Form(30))
assert(s.signal.now() == Form(30))

assert(effects.toList == Nil)

assert(d.tryNow() == Success(30))
assert(d.now() == 30)

assert(effects.toList == List(
"project-30"
))
effects.clear()

// --

s.update(f => f.copy(int = f.int + 1))

assert(s.tryNow() == Success(Form(31)))
assert(s.now() == Form(31))
assert(s.signal.now() == Form(31))

assert(effects.toList == Nil)

assert(d.tryNow() == Success(31))
assert(d.now() == 31)

assert(effects.toList == List(
"project-31"
))
effects.clear()

// --

s.tryUpdate(currTry => currTry.map(f => f.copy(int = f.int + 1)))

assert(s.now() == Form(32))
assert(s.signal.now() == Form(32))

assert(effects.toList == Nil)

assert(d.now() == 32)

assert(effects.toList == List(
"project-32"
))
effects.clear()
}

it("LazyStrictSignal: laziness and errors") {

val effects: mutable.Buffer[String] = mutable.Buffer()

val s = Var(Form(10))
val d = s.signal.mapLazy[Int] { form =>
effects += s"project-${form.int}"
form.int
}

val err1 = new Exception("Error: err1")

assert(s.tryNow() == Success(Form(10)))
assert(s.now() == Form(10))
assert(s.signal.now() == Form(10))

assert(effects.toList == Nil)

assert(d.tryNow() == Success(10))
assert(d.now() == 10)

assert(effects.toList == List(
"project-10"
))
effects.clear()

// -- Errors propagate

s.setTry(Failure(err1))

assert(s.tryNow() == Failure(err1))
assert(d.tryNow() == Failure(err1))

assert(effects.toList == Nil)

assert(errorEffects.toList == List(
Effect("unhandled", err1)
))

errorEffects.clear()
}
}

0 comments on commit 7b01cbe

Please sign in to comment.