tc39 / proposal-observable

Observables for ECMAScript
https://tc39.github.io/proposal-observable/
3.06k stars 90 forks source link

Observable should be async #206

Open vwkd opened 4 years ago

vwkd commented 4 years ago

If I understand correctly, Observables don't use the microtask queue unlike Promises. This makes Observables by default not async. This is weird. Even in the motivation it says

The Observable type represents one of the fundamental protocols for processing asynchronous streams of data

In the implementation RxJS Observables for example, the following logs the values 21 and 42 synchronously.

import { Observable } from 'rxjs';

const observable = new Observable(subscriber => {
  subscriber.next(21);
  subscriber.next(42);
});

console.log('first');
observable.subscribe(x => {
  console.log(x);
});
console.log('last');
first
21
42
last

I have to admit I didn't read the proposal, so this may be just in the RxJS implementation. But there hasn't been an issue filed neither there nor here, so it seems like this is intended behavior. I believe this should be subject to a serious discussion.

I don't see the point in synchronous push-based operations. One could just as well use a pull-based operation with the already existing Iterator. Actually it would look almost identical. Here's the above example using an iterator (created with a generator for brevity).

function* gen() {
    yield 21;
    yield 42;
}
const iterator = gen();

console.log("first");
for (const val of iterator) {
    console.log(val);
}
console.log("last");

The only real difference is that the consumer would need to loop through the values himself instead of having his "subscription" having looped automatically by the producer.

In conclusion, synchronous push-based operations make little sense, because they can just as well be implemented as pull-based using the existing Iterator. Push operations only really start to make sense with asynchronous operations, like with promises.

With observables being properly async, the above example would give.

first
last
21
42

Currently this needs to be manually ensured by wrapping the subscriber.next(..) calls in a Promise.resolve().then(() => {..}) or a setTimeout(() => {..}, 0) call (depending if you want to use the microtask queue or the normal queue).

Observables would make much more sense as generalised promises which can resolve multiple times, meaning they can push multiple values over time asynchronously. Everything else in the famous sync / async / single value / multiple values diagram is already covered well by the language.

I strongly believe it would be a lost opportunity to not use the microtask queue for Observables. We don't need to have another addition to the language, which an external library could fully implement, which is the case for Observables in the current synchronous fashion. Using the microtask queue would however be a real reason to include Observables into the language, because this is what a third-party library could not implement (without the Promise.resolve(..) hack).

SanderElias commented 4 years ago

The synchronous option is a feature.
It allows you to mix things you want to do synchronous, so you don't lose an event-cycle, and at the same time, be open to async. One use case might be loading for default values in a UI. You merge in the placeholder/preloaded content, this is then available during the first event cycle. This makes a smooth flicker-free operation possible. Especially if there are animations involved. Also, sometimes you have to take special care to the order things are executed. Doing things sync makes that a whole lot easier.

vwkd commented 4 years ago

Observables currently try to allow for both things, being sync and async. You say this is a feature.

As I showed in my post, for sync multi-valued operations there is already Iterator, which works just as well because for sync operations there is no difference between push and pull. Why didn't Promises also allow for sync usage? Because for single-value operations this is already provided by Functions. For the same reason Observables should be strictly async. There doesn't need to be a feature implemented twice.

But even if, even if Observables would allow for sync usage, it should not be the default behavior. A fully redundant behavior should not be the default.

ljharb commented 4 years ago

There is a difference between push and pull for sync, it’s just a smaller one.

benlesh commented 4 years ago

If Observables could not be synchronous, they could not model EventTarget, which can be registered and triggered synchronously.

adamf92 commented 3 years ago

Imagine situation, when you don't know if data will be delivered synchronously or asynchronously (in example, in UI framework, that I'm creating (Atom-iQ), I cannot know it, cause everything to process is coming from the framework consumer) - RxJS with sync observables provides a way to handle it, in exactly the same way. To be honest, for my framework it's the "must have" feature, far from calling it "redundant"