Re: [whatwg/dom] Improving ergonomics of events with Observable (#544)

@domfarolino So that's a LOT of questions, and I'll try to answer as many as I can. There are a couple of things worth noting here:

1. This was a proposal made on Google's behalf when I was at Google :) (I still believe in it, though)
2. Note that there's no part of this that is the `Observable` constructor. At the time it was thought to be too contentious to introduce a way to create observables from scratch (although I do think that the platform would benefit from this primitive). However, I'm amenable to adding a way to create observables given new versions of them pop up in the wild in many notable libraries.


> Why are Observables needed? Why is synchronous delivery important?

### Observables as a primitive

Observables, at a basic level, are more primitive than Promises, Streams, or Async Iterables. They are, for lack of a better way to put it: "Specialized functions with some safety added".

Observables could be in _many_ shapes, and don't need to be confined to what you've seen above in this proposal. Consider that a straw man. 

If we wanted to build the ideal PRIMITIVE `Observable`:

1. Some ability to define a "setup" or "init" function: A way to tie some data producer (Or a "subject" to some "observer", this is the "observer pattern") synchronously when you "subscribe" (call the specialized function, aka "observe", "listen", et al in the wild)
2. A way to notify a "consumer" of each new value _as soon as possible_ Such that when your data producing "subject" notifies, you can immediately notify whatever code cares. (In the wild this could be registering a listener, or handler, or observer, et al).
3. A way to notify a consumer that the producer is done producing values successfully
4. A way to notify a consumer than there was an error and the producer will stop sending values.
5. A means of the consumer telling the producer it no longer wants to receive values (cancellation, aborting, unsubscribing, et al)
6. Critically important safety if we're allowing people to _create their own observables_: A means of _tearing down or finalizing_ the data producer in any terminal case above (the producer is done, there was an error, or the consumer no longer cares).
7. Critically important safety if we're allowing people to _create their own observables: The consumer cannot be notified of anything after finalization (due to the producer being done, erroring, or unsubscribing)
8. A completely new call to the setup or initialization _per subscription_. (Note that you can still multicast with this via closure.)

### Observables in the DOM

In the DOM, we don't necessarily need the whole primitive (although it would be the most useful thing to have). Instead what the DOM needs is the consumer side of the contract. To make this really powerful, the ideal API is to have the whole primitive though.

> how critical are some of these constraints?

If you want to be able to accurately model what we can do with `EventTarget`, observables must be able to emit sychrnously.

```ts
console.log('start');
eventTarget.addEventListener('whatever', console.log);
eventTarget.dispatchEvent(new Event('whatever'));
eventTarget.removeEventListener('whatever', console.log)
console.log('stop');

// synchronously logs:
// "start"
// [object Object]
// "stop"
```

> I was surprised to find that it didn't cause any problems with the simple web app he wrote with his library

I'm not surprised. I'm amenable to having the observable primitive be asynchronous... however, it's decidedly LESS primitive once you force scheduling:

1. If it's synchronous, you can always compose scheduling in. If it's already scheduling, you can't force it to be synchronous. 
2. If it's not synchronous, it doesn't make a good cancellation notifier. (You'll notice that this proposal is trying to use `Observable` to cancel itself).

However, if it IS synchronous, there are definitely some ergonomics gotchas when working with it. For example if you're trying to write an operator in RxJS that may terminate a source (like `takeUntil` etc), you have to be careful to check to see if the cancellation notifier notified synchronously _before_ you even subscribe to the source. And what if you wanted to write a `takeOneAfterThisNotifies` sort of operator? It gets harrier.  This is why I'm amenable to `Observable` being asynchronous. However, I will say it does make it less useful (and probably more resource intensive) if it schedules (Again, the cancellation example above)


> Is "subscriber" the same as "observer"?

No. `Observer` would be an interface that really amounts to a few named handlers (`{ next, error, complete }`). Where `Subscriber`  implements that interface, and generally "wraps" some observer to add the safety in I talked about above. Making sure you don't call `next` after `error`, for example. Or binding the observation to the subscription so it can teardown.

> How big of a concern really, are the Promise-ifying APIs on Observable?

These things have been available for use for quite some time, pretty much since the dawn of Promises, and I haven't seen any real-world issues arise out of it. That said, promisifying observables is more of an ergonomics thing, since JavaScript has gone "all in" on Promise-related APIs (async/wait, for example).  The entire world is used to the lack of cancellation in Promises. That said, I could take the promise features or leave them. But again, I've never seen promise conversion be the issue for observables. In fact, we're going to add `[Symbol.asyncIterator]` handling to RxJS in the next version. The observable _primitive_ doesn't need them. It needs to simply _exist_ so people stop reinventing it and shipping it out in their libraries.

> How should I think about https://github.com/whatwg/dom/issues/544 vs https://github.com/tc39/proposal-observable/issues/201 (per https://github.com/whatwg/dom/issues/544#issuecomment-541763071)?

Think of it like this: @benlesh sees a pressing need to ship a real Observable primitive to the JavaScript community en masse, and I've tweaked my proposals several times over the years to try to reach an audience with the gatekeepers.

### Some facts:

1. When the TC39 proposal was made, RxJS (even the older microsoft version) was under a million downloads _a month_. RxJS is now at 47,000,000+ downloads _per week_.
2. Several popular libraries have _completely reinvented_ observables in their own code bases in various shapes, some of them as direct ports of RxJS's observable that were then altered, others just organically inventing their own (and maybe not really realizing it was an observable): 
  - React Router
  - XState
  - MobX
  - Relay
  - Recoil
  - Apollo GraphQL
  - Redux
  - Vue
  - Svelte
  - tRPC
  - RxJS
3. The debugging story for a native observable would be SO MUCH BETTER than what we can currently do with any of these homegrown "community driven" implementation.

I'm tired. I don't want to be the sole arbiter of the most popular Observable implementation. It's a primitive type. It should be in our platforms by any means necessary. We're shipping countless bytes over the wire to send these observables out to everyone, where if the _actual primitive existed_ I believe more library implementors would use them. Especially if they performed well because they were native, and they had a better debugging story in dev tools (because they're native). I want RxJS to be a cute little library of helper methods over a native type that becomes slowly irrelevant as those features grow in the native platforms. (Thus completing the long-running statement "RxJS is lodash for events").

### What would I find to be acceptable criteria for a good Observable type in the platform?

See the requirements above. No "operators". Doesn't need any promise conversion stuff. That's all just "nice to have". This would be fine:

```ts
const source = new Observable(subscriber => {
   let n = 0;
   const id = setInterval(() => subscriber.next(n++), 1000);
   return () => clearInterval(id);
});

const ac = new AbortController()
const signal = ac.signal;

source.takeUntil(signal.on('abort')).subscribe(console.log);
```

But for the sake of **this** proposal: The main thing it was tryingto do is make events more ergonomic.  Like:

```ts
eventTarget.on('click').takeUntil(signal.on('abort')).subscribe(() => console.log('clicked'))
```

Ironically, @benjamingr , a LONG time supporter of RxJS may have killed any chance the community had at getting Observable on a platform when he added `{ signal }` to `addEventListener`. 😄 

```ts
eventTarget.addEventListener('click', () => console.log('clicked'), { signal }); // This works today.
```

The down side? The API we have above with `addEventListener` doesn't compose. It can't communicate when it's "done". It doesn't stop when there was an "error". Use cases that are covered by a real observable like dragging something aren't as straightforward. Imagine this:

```ts
div.on('mousedown').flatMap(downEvent => {
  const divX = div.getBoundingClientRect().left;
  const startX = downEvent.clientX;
  return document.on('mousemove')
    .map(moveEvent => divX + moveEvent.clientX - startX)
    .takeUntil(document.on('mouseup'))
})
.subscribe(x => {
  div.style.transform = `translate3d(${x}px, 0, 0)`;
})
```

It would be pretty cool to be able to do that _without_ RxJS. The imperative version of that is easier to mess up in my opinion. And note that outside of `takeUntil`, the other methods are things that are already found in other JavaScript "iterable" sorts of things.  (`Observable` is the "inverse" of an `iterable`, BTW... which is another reason it's synchronous, if we wanted to be nerds about it)

This WHATWG proposal was painted as the _best chance_ Observable ever had. My discussions with @domenic years ago signaled that he didn't believe it belonged in the ECMAScript standard (so presumably it's blocked there), and it was his determination that proposing it here was the best idea.

---

I hope this answers all of your questions, @domfarolino, and I hope you found this helpful. Please feel free to ask more. You know where to find me. (My github notifications are a hot mess because of RxJS, and I'm not paid to work on anything in open source or on GitHub, so if you ping me here I might not see)

-- 
Reply to this email directly or view it on GitHub:
https://github.com/whatwg/dom/issues/544#issuecomment-1433955626

You are receiving this because you are subscribed to this thread.

Message ID: <whatwg/dom/issues/544/1433955626@github.com>

Received on Friday, 17 February 2023 01:13:45 UTC