raquo / Airstream

State propagation and event streams with mandatory ownership and no glitches
MIT License
247 stars 28 forks source link

Update and get #98

Closed olynch closed 2 years ago

olynch commented 2 years ago

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?

yurique commented 2 years ago

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

raquo commented 2 years ago

Right, now() does not require a transaction, but update() does, which means its execution is subject to transaction delay if it's invoked while another transaction is executing (e.g. inside an observer, as is typical).

So if you for example call update() and then now() inside an observer, the update will be delayed until the current transaction finishes, but now() will execute immediately and return the pre-update var state. At least that's what I think Owen means? https://github.com/raquo/Airstream#var-transaction-delay

As for solution - since transactions propagate depth-first https://github.com/raquo/Airstream#scheduling-of-transactions, I think you can just wrap your now() code inside your own transaction, like so:

myvar.update(...)
new Transaction { _ =>
  val b = makeB(myvar.now())
}

This way, makeB() and now() will be guaranteed to execute after update() but before any other unrelated transaction.

However, notice that we can't return a B from this code snippet - because the output of B can also be delayed, just like the execution of update(). So, strictly speaking it's impossible to make an operator like you want that would return B. Would my code snippet above solve your problem?

--Nikita

olynch commented 2 years ago

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 2 years ago

@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.

olynch commented 2 years ago

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!