-
-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Streams should emit in a shared transaction when starting #111
Comments
I don't remember having problems related to this, either. |
I think I fixed this. The fix is pretty simple, but I had to arrive at it the hard way, after trying out increasingly complicated mechanisms that did not work. The sufferingInitially I was operating under an assumption that I want or need to fire events that are emitted onStart by all of the streams listed in the Scope section in a shared transaction, similarly to how we handle events emitted onStart by It's hard to get right due to weird cases like But this is all moot, because the behaviour I was trying to achieve is actually undesirable. I realized that [1] Except the The actual solutionSo, that's what I did. Now, any transaction created inside I also fixed a bug where multiple Try it outI published That said, there could be slight differences in the order of events fired when mounting elements. For example, now If you try it out, please subscribe to this issue to get notified of any problems reported by other users. |
Problem description
Typically when a stream starts, it does not emit any events immediately, this happens later. For example,
parentStream.map(...)
does not emit anything on start (unlessparentStream
does),FetchStream.get(...)
needs to wait for the response, etc.However, some streams do emit events when they're starting (i.e. when they are getting their first subscriber), and that can cause arguably surprising behaviour. I don't remember running into that, recently at least, but here is a trivial example in Laminar:
(scribble)
You might expect the div to contain
1
and10
, but actually it will only contain1
. Why: because when the div is mounted, its subscriptions will be activated sequentially, one by one:First,
child.text <-- stream
will activate, this will startstream
, and the stream will emit an event (1
) in a new transaction. However, when this happened, we were not inside any other transaction, so the event has nothing to wait for, and will propagate immediately, being sent to child.text.Then,
child.text <-- stream.map(_ * 10)
will activate. Thestream.map(_ * 10)
stream will be started, and it will be added to the list of listeners onstream
, but nothing else will happen, becausestream
is already started at this point, and the event that it emitted on start (1
) has already been fully propagated – before this second subscription was activated. So, the streamstream.map(_ * 10)
will not receive any events, and will not emit any events, and this secondchild.text
will miss the event.If
stream
was instead a signal, everything would have worked as you'd expect, because signals remember their current value, but streams by design have no concept of current value.Importance
I think this is undesirable for a couple reasons:
It's unintuitive and not obvious. Looking at the code, you wouldn't expect it. You would get the expected behaviour if you changed
val stream
todef stream
, creating two identical but independent streams, but that's not the kind of finagling I want Laminar users to need to do.Running this same code inside a transaction (e.g. rendering this div inside
child <-- someOtherStream.map(...)
) will produce different results: in that case, since the1
event is emitted in a new transaction, that transaction will wait for the currently ongoing transaction to finish, and by the time this happens, both of the child.text subscriptions will have be added, so both of them will process that event.Scope
EventStream.fromValue(1)
is just the most obvious case. Any stream that emits events immediately when it's starting is affected by this problem. Here's an exhaustive list of such streams, I think:ConcurrentStreamStrategy
manually, the stream emits an error on start when its parent is a signal in an error stateEventStream[Signal[A]]
, the resulting SwitchSignalStream emits the signal's updated value on start IF its value was updated while the stream was stopped. Much like signal.changes, except... well, more on that below.Proposed solution
I mentioned
signal.changes
, but that one is different than the others, because in Airstream v15 I added special logic to it to handle something that is very similar to this problem, but is about re-starting those streams, not starting them for the first time. See the docs explaining that problem in this section. Relevant quote:So, I think all of those streams listed above, that emit events when staring, need the same kind of treatment that I gave to
signal.changes
in Airstream v15 – the code in theironStart
methods that creates a new transaction immediately, needs to instead schedule the new transaction to be executed at the end of the currentonStart.shared
block.Side effects
The proposed solution will result in our example executing as I would expect, the div containing both 1 and 10, that's good.
But it will also bring a more fundamental change: essentially, ALL of the events that are emitted like this – on start, or in Laminar, on mount – will now be treated as having had happened "at the same time", i.e. in the same transaction. On the surface, it seems like the right way to think about those events, since they all indeed had only one underlying event triggering them – the mounting – but the potential problem is, you normally wouldn't expect something like
stream
andstream.flatMap(...)
to emit in the same transaction, regardless of what's inside...
, but now, they could. It's a similar problem to whatsignal.changes
, but that one is a bit more niche, because it only applies to **re-**starting that stream, whereas this will now apply to more types of streams, and also when starting them for the first time, not just re-starting.As I work on this, I will come up with a couple test cases demonstrating the side effect, it's a bit too involved to do right now. I hope that this will not be a significant problem.
What now
I'd like to know if you have encountered the problematic behaviour, and how surprising / annoying it was. For myself, I don't remember running into it, as I tend to use signals for anything that remotely resembles state, keeping streams only for things like user clicks for the most part, and those types of events just don't tend to happen on start / on mount.
I haven't started implementing this change yet, but I have a good idea of what needs to be done. If it actually works the way I think it will, I think the fix will be binary compatible, and I will release Airstream v16.0.1-M1, which you'll hopefully be able to try without updating any other dependencies. I don't want to publish a non-milestone 16.0.1 version because I don't want Scala Steward or other tooling suggesting it as a trivial update. I think "M1" should be enough to dissuade people anyway? If all is well then I'll eventually bring these changes into v17 a few months from now, but I do want a "preview" release that has nothing except these changes, to fish out any issues early on.
The text was updated successfully, but these errors were encountered: