Skip to content
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

.split operator is hard to use without Laminar #101

Open
raquo opened this issue Sep 29, 2022 · 0 comments
Open

.split operator is hard to use without Laminar #101

raquo opened this issue Sep 29, 2022 · 0 comments
Labels
design need to find time https://falseknees.com/297.html

Comments

@raquo
Copy link
Owner

raquo commented Sep 29, 2022

  • This is based on the upcoming 15.0.0 code, but I'm pretty sure the issue applies just as well in all prior versions.
  • This complex implementation detail is kinda hard to explain sufficiently well, so I'm writing this mostly as a TODO for myself.
  • If you did find the title of this issue to be an obstacle for yourself, please do let me know.

The split operator creates child signals for every new key it encounters. Complex implementation details aside, they are conceptually similar to parentSignal.composeChanges(_.collect(fn)), where fn looks for memoized output associated with the key that this signal is bound to, and returns that.

In Laminar, you typically use split to render lists of children, so you can have code like this:

val $listOfModels: Signal[List[Model]] = ???
def renderCallback(key: Int, initial: Model, $model: Signal[Model]): HtmlElement = {
  span(child.text <-- $model.map(_.name))
}
div(
  children <-- $listOfModels.split(_.id)(renderCallback)
)

Here, we use data from the child signal ($model) to render the name for each model, updating it without rebuilding the DOM. Laminar manages all of the <-- subscriptions with custom DynamicOwner logic, where it activates Subscription-s only when the element to which <-- is bound is mounted into the DOM, and kills them when the element is removed from the DOM.

That logic is exactly what's causing the child.text <-- $model.map(_.name) subscription to be killed when the key it is tied to stops appearing in $model. The problem with that is now obvious – that logic lives in Laminar, not in Airstream. In pure Airstream, you will find it exceptionally hard to subscribe to the $model signal in a way that would safely dispose of the subscription once the corresponding key is removed from the $listOfModels signal – user code in renderCallback is simply not notified of that, nor is it provided an Owner that would be safe to use for custom subscriptions.

$listOfModels.split(_.id)((key: Int, initial: Model, $model: Signal[Model]) => {
  $model.foreach(println _)(owner = ???) // Without Laminar, you don't have a suitable key-specific Owner to use here
}

I only realized this from testing new split features, and I'm not really surprised that nobody has complained so far – I doubt Airstream sees much use without Laminar, since by Airstream's design you want a consuming library to manage ownership in a boilerplate-free way. So, I'm not particularly in a rush to fix this, but this issue needs to be documented somehow, so here it is.

Some day I'd like to fix this. I'm not sure how to do it well though. Perhaps when if eventually implement observable completion (#23), the split operator could complete the child signal it created when removing the key associated with it. That would actually be pretty easy, just need to track the child signal in memoized in a tuple together with Output, or something like that.

Even without observable completion, I guess we could also implement custom logic inside SplitChildSignal that would stop the signal and permanently disable it, preventing new observers from restarting it. This would be very non-standard behaviour for Airstream though.

Both of these solutions assume that you have access to an Owner in the parent scope – one that will kill the whole stream.split(...)(...) structure when its result is no longer needed, and that you will use that same Owner for each $model. This is an acceptable assumption to me, but alternatively, Airstream could potentially provide a special Owner as the fourth argument in the project callback, however I don't like this because you don't need this owner in Laminar, and that owner existing will just confuse people.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design need to find time https://falseknees.com/297.html
Projects
None yet
Development

No branches or pull requests

1 participant