-
Notifications
You must be signed in to change notification settings - Fork 331
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
What to do with Bus().toProperty()
?
#729
Comments
Maybe this is not so bad: Letting the application actively decide for lazyness or eagerness. Let's follow this path for a moment: // pseudo-code
var prop = observers.length
.map(l => l > 0)
.skipDuplicates()
.flatMapLatest( areAny => areAny ? createProperty(...) : Bacon.never() );
observers
.onValue(o => prop.onValue(o)); On the other hand, when keeping the property eagerly up-to-date is no problem for the application, then it's just the "eager-making" the application is responsible for. In the other case, if one wants to retain lazyness and be able to re-subscribe to property - I have no idea. On re-subscription one would have to travel backwards in time and replay all events which lead to the current value. Any 🤔 |
Why is it counterintuitive? Then why not make all Properties eager? This would acknowledge that a Property ("thing with a state") has to be kept up-to-date regardless of its observers until it is garbage-collected by the JS runtime. I think the current description of Properties does not tell the whole story anyway. Should it not rather be (emphasised text added, striked-out text goes away in case of all-eager properties):
In the proposal
would |
A lot of questions in a single comment by friend! Making all properties eager would, if I understand your suggestion correctly, prevent them from being garbage-collected because they are referenced by the underlying streams keeping them up to date. Unless, of course, we somehow were able to use weak references for the supply chain. Unlikely so. I don't really believe my patchy little suggestion above would be a good idea. Would make one case work better but still leave room for error in more complex setups. Let's just forget about it. |
Considering the Lazyness of Properties an observation which is not clear to me: Right now a subscription to an observable turns it's dependency tree of combined observables eager (i.e. stateful). With the direction from bottom to top. /* p1 p2 ^
\ / | eagerness travels upwards
p1orp2 |
*/
var p1 = Bacon.repeatedly(11000, [true, false]).toProperty(false);
var p2 = Bacon.repeatedly(15000, [true, false]).toProperty(false);
var p1orp2 = p1.or(p2);
// this makes p1orp2 and its dependencies p1 and p2 stateful
p1orp2.onValue(x => x); // false, true, true, true, false, true, false, true ... However, does the eagerness of the tree also travel down into the combined trunk? I mean, does the subscribing to // p1, p2 and p1orp2 see above
var p1, p2, p1orp2 = ...
// just make the p1 and p2 dependencies eager
p1.or(p2).log("p1orp2");
Bacon.mergeAll(
Bacon.later(7000, "A"),
Bacon.later(12000, "B"),
Bacon.later(19000, "C"),
Bacon.later(35000, "D")
).flatMapConcat(
x => Bacon.once(x).holdWhen(p1.or(p2)).delay(500)
).log();
// p1orp2 false
// A <-- correct because no holding
// p1orp2 true, p1orp2 true, p1orp2 true <-- correct no emissions
// p1orp2 false B C <-- correct B is released now and C passes because no holding
// p1orp2 true D <--- WRONG D must not pass while p1 || p2 is true
// <end> With bottom up eagerness the example works like expected. The code is nearly identical but one has to introduce a seemingly useless variable and not forget to make it eager explicitly. // p1, p2 and p1orp2 see above
var p1, p2, p1orp2 = ...
// make the whole tree eager;
p1orp2.log("p1orp2");
Bacon.mergeAll(
Bacon.later(7000, "A"),
Bacon.later(12000, "B"),
Bacon.later(19000, "C"),
Bacon.later(35000, "D")
).flatMapConcat(
x => Bacon.once(x).holdWhen(p1orp2).delay(500)
).log();
// p1orp2 false
// A <-- correct because no holding
// p1orp2 true, p1orp2 true, p1orp2 true <-- correct no emissions
// p1orp2 false B C <-- correct because no holding anymore
// p1orp2 true
// p1orp2 false D <-- correct because no holding
//<end> So if eagerness would propagate also from top to bottom one could write Anyway I don't understand why sometimes a combined property seems to "work a little" even though |
To me, it makes perfect sense that 'SECOND' will be missed. It’s maybe unintuitive at first, but once you realize how observables work, anything else would be unintuitive.
I prefer and rely on How about simply dropping its state if it was obtained lazily? That's a can of worms, though. If a |
I still don't think that makes sense. How can the first item in a multicast property stream depend on the temporal order of subscriptions from other consumers in entirely different modules of the application? The current subscriber has no idea when other subscribers hook into the property stream. How would you model such a trivial thing as a multicast property of the bowser's window size in baconjs? var windowSize =
Bacon.fromEvent(window, 'resize')
.map(evt => ({width: evt.target.innerWidth, height: evt.target.innerHeight}))
.toProperty({width: window.innerWidth, height: innerHeight});
var unsubscribe = windowSize.onValue(console.log.bind(console)); In temporal subscription gaps to In my applications I carefully pay attention to manage a continuous lifetime of the such multicast properties. I think I am all-in for throwing an exception on resubscription to a property. |
It makes sense to me because
(Emphasis mine) Ah, now we're talking about something else – values being pushed to var windowSize =
Bacon.fromEvent(window, 'resize')
.map(evt => ({width: evt.target.innerWidth, height: evt.target.innerHeight}))
.toProperty({width: window.innerWidth, height: innerHeight});
var unsubscribe = windowSize.onValue(console.log.bind(console));
You should also be all-in for throwing an exception on the very first subscription to This illustrates the danger of Here’s my proposal:
Using the above proposal, your var windowSize =
Bacon.fromEvent(window, 'resize')
.map(evt => ({width: evt.target.innerWidth, height: evt.target.innerHeight}))
.toProperty(() => ({width: window.innerWidth, height: innerHeight})); When the subscription count increases from 0 to 1, the callback passed to Alternatively, if there's a reasonably concise and expressive way to achieve this with current Bacon constructs, then |
That's the technical explanation of the behaviour but not the explanation why in the context of reactive streams one should expect such a behaviour.
Yes, making
windowSize = Bacon.fromBinder(sink => {
sink({width: window.innerWidth, height: innerHeight});
window.addEventListener('resize', sink); // I left out mapping from ResizeEvent to {width: Number, height: Number}
return () => {
window.removeEventListener(sink);
};
})
.toProperty(); Should do the job but it is not as pretty as Edit: |
I thought “reasonably concise and expressive” implied “Don’t use
The idea is to make
I wouldn’t want to have to provide a var windowSize =
Bacon.fromEvent(window, 'resize')
.map(evt => ({width: evt.target.innerWidth, height: evt.target.innerHeight}))
.toProperty(() => Promise.resolve({width: window.innerWidth, height: innerHeight})); Also, I wouldn’t want |
Lazy evaluation means that the value is computed when needed and not at the time when the property is defined.
There are plenty of examples in the browser or node.js APIs where you cannot pull a value synchronously (e.g. Anyway – returning a promise or not – it does not solve the problem where the values are pushed into the property from elsewhere (e.g. WebSocket) and the current value cannot be pulled in. Contrary to intuition values are lost in a non-plausible way. Edit: |
Think about it for a moment. If the part of the stream before For example: const windowSize = Bacon
.fromEvent(window, 'resize')
.map(evt => ({width: evt.target.innerWidth, height: evt.target.innerHeight}))
.toProperty(() => new Promise(resolve => (
const listener = evt => {
// Duplicate event - will also be emitted by fromEvent
resolve({width: evt.target.innerWidth, height: evt.target.innerHeight})
window.removeEventListener('resize', listener)
}
window.addEventListener('resize', listener)
)) This is a bit contrived, but I think it illustrates the point. |
What I meant was something like ...
.toProperty(() => new Promise(resolve => navigator.geolocation.getCurrentPosition(resolve))); |
Presumably, this is the part before const position$ = Bacon
.fromBinder(sink => {
const handle = navigator.geolocation.watchPosition(sink)
return () => Geolocation.clearWatch(handle)
}) What’s the point of asynchronously resolving the first event when we’re going to do it anyway via |
Yes, if we use Imagine one had written a Bacon.fromGeolocationChangesEventStream()
.toPropery(() => new Promise(resolve => navigator.geolocation.getCurrentPosition(resolve))); in order to have the current position asap and not wait for movement to get a fix on the location. (Just another made-up example, please take it with caution) |
The first subscriber to Suppose we had |
Because it's fun, another example without var clipboardText =
Bacon.fromEvent(document, 'paste')
.flatMapLatest(() => Bacon.fromPromise(
navigator.clipboard.readText()
))
.toProperty(() => navigator.clipboard.readText()); // <-- returns a promise |
If |
Hi guys! I wrote something on property reactivation on #770. What do you think? |
I've read (and answered to) numerous complaints that code involving
bus.toProperty()
"does not work". And I totally agree that it's counterintuitive that something like this (from Bacon FAQ) does not work:You'd like the console to log 'SECOND' but it logs 'FIRST' instead.
What's happening here is that your Property won't get updated when there are no listeners. Before you add an actual listener using onValue, the Property is not active; it's not listening to the underlying Bus. It ignores input until someone's interested. So it all boils down to the "laziness" of all Bacon streams and properties. This is a key feature too: the streams automatically "plug in" to their sources when a subscriber is added, and "unplug" when there are no more subscribers.
A simple patch for this would be to have
Bus::toProperty()
always add a dummy subscriber to the resulting Property. Then, again, it would fail in setups like this:... so it's not such a great patch after all. Or is it? Because if you flipped it like
it would be good again!
The underlying problem is that the subscriber-count-based mechanism for change propagation in Bacon.js doesn't serve you well in cases when you create a stateful Property on top of a stateless EventStream unless you always have at least one subscriber on the Property.
One option I'm half-seriously considering is to have Properties throw an error in cases where you re-subscribe after the last subscriber has been removed. This is probably always a bug in application code, because it can lead to propagating outdated values.
Thoughts?
The text was updated successfully, but these errors were encountered: