-
-
Notifications
You must be signed in to change notification settings - Fork 141
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
Design problems with async stop #104
Comments
I liked the non-restarting behavior more. The restarting lowers the streams temperature, and referential transparency isn't really compatible with hotness |
Is it possible to remove |
Btw, the same behaviour can be achieved without async stop: switching over addition and removal lines might be enough: https://github.com/staltz/xstream/blob/master/src/core.ts#L649,L650 |
I personally prefer maintaining the hot/cold distinctions, but I feel like the philosophy of xstream is to prefer hot-always behavior (non-restarting) to referential transparency, so I'd vote for that. If non-restarting wins out, what would this code print out?
|
it will be |
There are cases where non-restarting makes a lot of sense, and where restarting makes more sense. Also, as a reminder, non-restarting behavior can also happen in flatten even when switching to different streams. const a$ = // ...
const b$ = // ...
a$.addListener(l1);
b$.addListener(l2);
const c$ = xs.periodic(1000).map(i => {
if (i % 2 === 0) {
return a$;
} else {
return b$;
}
}).flatten(); Neither a$ nor b$ will restart when the switch happens in flatten, because these already have listeners, so they are executing no matter what. One way of looking at this behavior of map+flatten is
One way we can keep this distinction is to add That example code from @ntilwalli reminds me of issue #91, which I'm working on in another branch and although interesting, has not much to do with this issue. I'll submit that PR soon for discussion.
Even if we do remove async stop, we still need to choose between restarting or non-restarting semantics. |
In this case non-restarting behaviour could be modeled using such tricky trick: const a$ = // ...
const b$ = // ...
const c$ = xs.merge(a$.drop(), b$.drop(), xs.periodic(1000)).map(i => {
if (i % 2 === 0) {
return a$;
} else {
return b$;
}
}).flatten(); where |
when synchronously removing last listener and adding a new listener, but this use case is so rare that I'd say the main use case is |
Maybe I'm too noob to opinate on this, but, does it make sense to make |
Everything is possible with options, but it's important (specially for xstream) to have smart defaults and "just works" behavior. |
Options is not needed here of course. Need to list and consider all pros and cons non-restarting/restarting brings to the table. |
After thinking about this for a few days, maybe settling with non-restarting is better, while accepting lack of referential transparency. restarting is incompatible with non-restarting, but restarting is also incompatible with use cases of multiple listeners, see below: const sum$ = inc$.fold((x, y) => x + y, 0);
sum$.addListener(/* ... */);
const lastSum$ = refresh$.map(_ => sum$).flatten();
// not the same as:
const lastSum$ = refresh$.map(_ => inc$.fold((x, y) => x + y, 0)).flatten();
// because the sum$ will not restart since it always
// has the listener on the second line.
// While we keep the guarantee that each stream has only one execution. I think the best way to resolve this is keep non-restarting behavior consistent, and then add Please 👍 if you agree. Comment if you don't. |
@staltz also it would be better to get rid of |
Okay, let's close this because the conclusion is to implement #7. |
Because of issues cyclejs/cyclejs#365 and #90, I made a fix to xstream which gives special treatment to a corner case in
flatten
, which was released in v5.3.2. 819bc94However, (1) it doesn't actually fix the problem, (2) it conflicts with previously desired features. It also makes new bugs appear such as #103.
(1)
Note how we added this test, which passed in v5.3.2:
However, this does not pass in v5.3.2:
Because every time the function
() => inner.map(x => x)
is called,inner.map(x => x)
will yield a different stream, where as() => inner
would always yieldinner
as the same stream.(2)
Sync start and async stop was designed to allow the inner stream to not restart if it was the same during the switch in a flatten. This was really by design, to avoid some confusion with a common pattern we had in Cycle.js, the use of RxJS
connect()
here: cyclejs/cyclejs@67d176e#diff-32a0a3abed94d032137ef603ee4dd261L30So "don't restart the inner stream if it remains the same during flatten" is a feature by design.
However, to have referential transparency we want these two cases to give the same behavior:
Which means we want the property "restart the inner stream if it remains the same during flatten" by design.
Which means we have a conflict, and we need to choose which of these two to do. It may mean a breaking change. I had hopes sync start and async stop would make things more intuitive but there is an obvious drawback that makes xstream less intuitive.
If you're reading this thread, please give your friendly and thoughtful opinion on this topic. This appears to be my design mistake, but I'm just a human. What's important is that I'm willing to look for a better way forward. My intent with sync start and async stop was to provide an "just works" experience for most cases, and together with Tylor we did a lot of predictions and bike-shedding, but a corner case slipped out of our sight.
For now, I'll revert the bugfix that happened in v5.3.2, so that other issues don't surface, and to keep a consistent behavior.
The text was updated successfully, but these errors were encountered: