raquo/Airstream

Update and get

olynch opened this issue · 5 comments

I want there to be a method on a Var[A] that takes in a function A -> (A,B) and returns a B. This would be a generalization of update. This can be simulated using now() and set(), but then there are two separate transactions.

Also, is there a way of doing this without modifying Airstream itself that I am missing?

I don't think now() involves a transaction 🤔

raquo commented

Sorry for taking a while to get back to this; I'm just now opening up my laminar project again. The problem is that I'm worried that that the following code is incorrect:

val a = $a.now()
val (newA,b) = f(a)
$a.set(newA)

I.e., I'm worried that in between getting the value of $a and setting it, something else could write to $a and then that write would be forgotten. But I guess because JavaScript is single-threaded that's actually not a problem?

raquo commented

@olynch Although Javascript itself is single threaded, because of Airstream's transaction system, the $a.set method is not guaranteed to actually update the Var immediately, it can de delayed until the current transaction is done. See https://github.com/raquo/Airstream#var-transaction-delay for details.

So yes, if you just write that snippet of yours, your other code can indeed change the value of $a after you called $a.now(), but before you called $a.set, for example if you have the following all inside the same Observer callback:

$a.set(ignoredA) // this does not update `$a` immediately because we're inside of an observer
val a = $a.now() // this reads the value immediately, so it reads the original value from `$a`, not `ignoredA` which wasn't set yet
val (newA,b) = f(a)
$a.set(newA) // this is also run at a delay, but it will run after the first `$a.set` above, overriding `ignoredA`.

As I mentioned, to solve this, you need to wrap the code that you want to run together/unbroken in a new Transaction. There are several ways to do it, but I guess the most obvious is this:

$a.set(firstA) // put this here, so that its transaction executes before the following `new Transaction`
new Transaction { _ =>
  val a = $a.now() // will read `firstA`, because this `new transaction` will be run after the `$a.set(firstA)` transaction.
  val (newA,b) = f(a)
  $a.set(newA)
}

For this to work, any $a.set / $.update that you do before $a.now() must be outside of new Transaction, if you want $a.now() to see those updates.

Notice that you can't return B from the new Transaction block, for similar reasons why you can't return B from an expression that is Future[B] – the B is not available yet.

We could potentially make Transactions return a value and provide a Future-like API with onComplete to get the value when it's ready, but there would need to be some real strong justification for that.

OK, this is actually fine though, because I could actually wrap this in a future and then pass b into the callback. This is already inside of some async stuff, so this meshes well. I think you can close this issue now. Thank you so much for helping me out here!