-
Notifications
You must be signed in to change notification settings - Fork 90
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
Research "eager" notification #128
Comments
Would this require Observables to send notifications asynchronously, on the next tick for instance? Would that mean that every call to |
Currently we catch errors that are thrown from the subscriber function and attempt to send them to the observer's If an error is thrown from let observable = new Observable(sink => {
setTimeout(() => { sink.next(1); }, 100);
});
observable.subscribe({
next(v) { throw new Error('from observer') },
error(e) {
// This will not receive the error above ^^^
},
}); Except it will if the observable emits eagerly! let observable = new Observable(sink => {
sink.next(1);
});
observable.subscribe({
next(v) { throw new Error('from observer') },
error(e) {
// This will receive the error above ^^^
// WTF Observable!
},
}); If we eliminated eager notification, then I suppose that we would need to change the |
Each notification would still be sent synchronously, you just (somehow) wouldn't be allowed to send notifications before
No. We would need to introduce latency for Rather than attempting to design a solution though, I'd like to understand what use cases absolutely require eager notification. |
@zenparsing how would you implement an
Promises do this too: new Promise(() => { throw new Error(":("); }); // rejects the promise
new Promise(() => setTimeout(() => { throw new Error(":("); })); // throws globally This is just how wrapped async code (like promises/observables) works with raw code, if you used |
@benjamingr The only difference for I'm not proposing that we make "next" calls async. I'm only trying to understand if "eager" notification (defined above) is a requirement. Also, let's not bring promises into this discussion, as I find the comparisons to be incredibly confusing. |
That makes sense (for
You've made that plenty obvious. I think @staltz had some good motivating examples for eager notification.
Promises were just to show it's not something we should aim to solve for the general case (your setTimeout example). |
Gotcha. The example I posted above is just meant to illustrate that the current behavior, where in some cases |
Let's not use the term "sync subscription" because I'm not proposing that we make "subscribe" async either. I just want to know whether there are use cases for eager notification. |
Roger. Going through our Rx code - this change would not cause any issues for us. I tend to be +1 on this change. |
One use case that I've seen for eager notification is "Property", described in #83. Basically, it's an observable for a sequence of changes to a "property" value. The user wants to see the current value immediately when they subscribe. I illustrated a non-eager alternative here in which the current value is sent in a future turn, if the property is not updated beforehand. |
@zenparsing pinging @mweststrate who wrote MobX: Michel - MobX does this (run the function synchronously in autorun). We're discussing changing the semantics of native ES observables to emit In MobX terms this means |
Similar in spirit to Property is ReplaySubject. On subscribe, it immediately sends out a buffered list of previous notifications (leaving aside custom schedulers). If we couldn't send notifications eagerly, ReplaySubject would have to delay subscription for a tick. |
Wow, this is frustrating. This was argued and settled literally a year ago. Observables are a primitive. If you want scheduling, compose it in. |
@trxcllnt That is not a helpful comment. |
Given that we'd like to be able to model EventTarget with Observable, I don't think we can give up eager notification. The following all happens in the single job on the event loop: console.log('start');
const handler = e => console.log('clicked');
document.addEventListener('click', handler);
document.dispatchEvent(new Event('click'));
document.removeEventListener('click', handler);
console.log('end');
// "start"
// "click"
// "end" You can't model that with Observable without eager notification. And in scenarios where you might be synchronously dispatching N-events and you only want to take a few of them, you'd be forced to buffer all of those values in an unbounded buffer (or via closures in memory) while you're scheduling. Also, given the nature of Observables as being a template to create a bridge of observation between a data producer and an observer, adding unnecessary scheduling through the middle of that observation chain isn't really going to help anyone, and will have some performance costs. I've brought this up before and my concerns were minimized, but the costs are there, I've had to measure them in the past. And at scale, in Node, for example, it's going to matter. Observables are a primitive. You can compose in scheduling, you can't compose in removal of scheduling. |
This is false. Please re-read the definition section above. |
I'm sorry, but it is frustrating. The more the spec deviates from proven, battle-tested prior art, the more buggy edge cases are introduced or possible use-cases precluded. It's frustrating because I do this all day, every day, for money. A huge part of my job is solving really hard problems (async microservices on distributed GPUs, yay!), pushing Observables to the max. Changing core stuff like this breaks things like our carefully designed high performance recursive async back-pressure-sensitive GPU-sync'd rendering loop. The es-observable spec is trying to improve on an idea that has years of academic and industry research behind it. I literally work with people whose PhD work inspired Rx. Yet we keep proposing fundamental changes to the core behavior for aesthetic reasons, without any research, analysis, or testing as to the implications. Observable is a like finely tuned equation, you can't just change the order of operations and expect it will still work. I'll try to go back and dig up all the arguments and examples, some made in this repo, some made in others, when I'm back to my laptop. |
@trxcllnt I sympathize, and for what it's worth, I'm in complete agreement that we should stick to the battle-tested design as closely as possible. I'm just concerned that this one aspect (eager notification) is really confusing and tricky. I think we both know that the eager notification leads to all kinds of little edge cases that must be dealt with carefully. Any input here would be great, but I want it to stay focused on eager notification use cases (like ReplaySubject). I'd like to put together a list. |
How would this work? Observable.range(0, 1000000000000).take(5) or this? Observable.from((function* () {
let i = 0;
while (true) yield i++;
}()).take(5) In order to accomplish what you're saying, you'd need to queue-schedule all calls to Unless this is an angle to try to get coroutine-type behavior out of Observable. Which still wouldn't prevent convoluted solutions to the above problems. |
Observable.range = function(from, to) {
return new this(sink => {
enqueueJob(() => {
for (let i = from; i < to; ++i) {
if (sink.closed) { return; }
sink.next(i);
}
sink.complete();
});
});
}; Same goes for |
@zenparsing the example here was meant to illustrate a case where asynchronous subscription precludes solving some task, in that case a recursive DFS, as async subscription implicitly requires trampolining, which is a breadth-first strategy. Unfortunately it seems the message was lost in translation, perhaps because many people don't do potentially-async DFS in their day jobs. The Falcor-Router project is an example of Observables in the wild that relies on synchronous subscription. Routes return Observables (or things we can make into Observables) that produce values. We have to get those values synchronously, as they're inserted into a graph of aggregated results, and that graph decides which routes to execute next. |
I think that implementation is missing a check of So everyone that wants to create a synchronous observable needs to have access to this If someone wants to notify the moment a WebSocket starts trying to connect, they can't do it, unless they have this // this isn't possible with the proposed change.
const measuredData = new Observable(observer => {
observer.next({ type: 'START', ts: performance.now() });
doSomeAsyncThing((data) => {
observer.next({ type: 'END', ts: performance.now(), data });
observer.complete();
});
}); Given that this is the only real goal of this issue:
If a major point of this design change is to improve understandability, I think it's plainly failed. The ergonomics are just as bad if not worse. Users are still required to know that sometimes observables are synchronous and sometimes they're not, but they must also understand that calls to |
Right, fixed.
I don't understand this use case.
That seems like a reasonable use case, thanks. Please keep in mind that I'm not advancing any proposal. I'm just trying to better understand why we allow this behavior that causes a bunch of trouble (like #119, apparently). Also, I'm really exhausted from arguing about this stuff. |
@trxcllnt Thanks for the links (although it will probably take me some time to unpack them 😄 ) |
Sorry, they're basically the same use case. Measuring a time to connection or the like. |
Same. I hope none of this seems personal. Although I can understand @trxcllnt's frustration because I feel like we've talked about several of these issues before, I think it's probably worth rehashing them because it's been a while. After all, these are questions that I'm sure the committee will bring up multiple times, as I'm sure a lot of them don't have their heads as fully into this proposal as some of us do. They're busy people and there are a lot of proposals. |
Oh right, I forgot |
@trxcllnt - mind elaborating on I also don't understand why trampolining forces var graph = {
a: {b: {c:true}, d: {}},
e: {}
};
function dfs(g, predicate) {
for(const edge in g) {
//console.log(edge, predicate(g[edge]));
if(predicate(g[edge])) return g[edge];
var rec = dfs(g[edge], predicate);
if(rec) return rec;
}
}
dfs(graph, x => x === true); // returns true, and if you uncomment the log logs a,b,c Now, let's make the DFS async with promises which trampoline (let's make graph expansion async and the predicate async: const getEdges = async o => Object.keys(o);
async function dfs(g, predicate) {
for(const edge of await getEdges(g)) {
if(await predicate(g[edge])) return g[edge];
var rec = await dfs(g[edge], predicate);
if(rec) return rec;
}
}
dfs(graph, async x => x === true) Now that both |
@benjamingr. Because as soon as you get a new value to group you have to create a subject or observable and next into it synchronously. In order for any consumer to use that value they have to be able to subscribe also synchronously. |
@Blesh yeah that's a much more motivating use case than the DFS :) |
@benjamingr sorry, should have clarified. the DFS behavior isn't deterministic -- it's contingent on whether falcor-router route handlers return synchronous Observables (or Arrays, etc.). This is by design. async-await doesn't allow sync notification, so it can't model the problem. And that's the point -- forcing async subscription necessarily limits the number of possible systems that can be expressed. |
@trxcllnt cool, I understand. It's generally anything that's groupBy "like". |
Definitions:
subscribe
has completed.subscribe
.Motivations:
subscribe
call has returned. The code which performs thesubscribe
cannot generally know whether notifications will arrive "eagerly" or not, and users will commonly make the invalid assumption that notifications will not. In the past, JS has attempted to eliminate these kinds of confusing conditions.observer.start
method.Goals:
Open Questions:
Related Previous Discussion:
next
(or similar) prevents unsubscribing #83Contributing:
Let's try to keep the discussion focused the stated goals, and avoid open-ended back and forth. If you can, try to provide some insight on one of the open questions listed above.
The text was updated successfully, but these errors were encountered: