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

Improving ergonomics of events with Observable #544

Open
benlesh opened this Issue Dec 13, 2017 · 104 comments

Comments

@benlesh
Copy link

benlesh commented Dec 13, 2017

Observable has been at stage 1 in the TC-39 for over a year now. Under the circumstances we are considering standardizing Observable in the WHATWG. We believe that standardizing Observable in the WHATWG may have the following advantages:

  • Get Observable to web developers more quickly.
  • Allow for a more full-featured proposal that will address more developer pain points.
  • Address concerns raised in the TC-39 that there has not been sufficient consultation with implementers.

The goal of this thread is to gauge implementer interest in Observable. Observable can offer the following benefits to web developers:

  1. First-class objects representing composable repeated events, similar to how promises represent one-time events
  2. Ergonomic unsubscription that plays well with AbortSignal/AbortController
  3. Good integration with promises and async/await

Integrating Observable into the DOM

We propose that the "on" method on EventTarget should return an Observable.

partial interface EventTarget {
  Observable on(DOMString type, optional AddEventListenerOptions options);
};

[Constructor(/* details elided */)]
interface Observable {
  AbortController subscribe(Function next, optional Function complete, optional Function error);
  AbortController subscribe(Observer observer); // TODO this overload is not quite valid
  Promise<void> forEach(Function callback, optional AbortSignal signal);
 
  Observable takeUntil(Observable stopNotifier);
  Promise<any> first(optional AbortSignal signal);

  Observable filter(Function callback);
  Observable map(Function callback);
  // rest of Array methods
  // - Observable-returning: filter, map, slice?
  // - Promise-returning: every, find, findIndex?, includes, indexOf?, some, reduce
};

dictionary Observer { Function next; Function error; Function complete; };

The on method becomes a "better addEventListener", in that it returns an Observable, which has a few benefits:

// filtering and mapping:
element.on("click").
    filter(e => e.target.matches(".foo")).
    map(e => ({x: e.clientX, y: e.clientY })).
    subscribe(handleClickAtPoint);

// conversion to promises for one-time events
document.on("DOMContentLoaded").first().then(e => …);

// ergonomic unsubscription via AbortControllers
const controller = element.on("input").subscribe(e => …);
controller.abort();

// or automatic/declarative unsubscription via the takeUntil method:
element.on("mousemove").
    takeUntil(document.on("mouseup")).
    subscribe(etc => …);

// since reduce and other terminators return promises, they also play
// well with async functions:
await element.on("mousemove").
    takeUntil(element.on("mouseup")).
    reduce((e, soFar) => …);

We were hoping to get a sense from the whatwg/dom community: what do you think of this? We have interest from Chrome; are other browsers interested?

If there's interest, we're happy to work on fleshing this out into a fuller proposal. What would be the next steps for that?

@annevk

This comment has been minimized.

Copy link
Member

annevk commented Dec 13, 2017

Thanks @benlesh for starting this. There've been mumblings about a better event API for years and with Chrome's interest this might well be a good direction to go in.

Apart from browser implementers, I'd also be interested to hear from @jasnell @TimothyGu to get some perspective from Node.js.

Another thing that would be interesting to know is what the various frameworks and libraries do here and whether this would make their job easier.

From what I remember discussing this before one problem with this API is that it does not work with preventDefault(). So if you frequently need to override an action (e.g., link clicks), you can't use this API.

cc @smaug---- @cdumez @travisleithead @ajklein

@davidkpiano

This comment has been minimized.

Copy link

davidkpiano commented Dec 13, 2017

From what I remember discussing this before one problem with this API is that it does not work with preventDefault(). So if you frequently need to override an action (e.g., link clicks), you can't use this API.

Couldn't this be covered with EventListenerOptions options?

element.on('click', { preventDefault: true })
  .filter(/* ... */)
  // etc.
@gsans

This comment has been minimized.

Copy link

gsans commented Dec 13, 2017

Cool! I propose to call them DOMservables

@matthewp

This comment has been minimized.

Copy link

matthewp commented Dec 13, 2017

@annevk Can you explain the preventDefault() problem in more detail? From @benlesh's examples, I would think you could call it in any of the filter() or map() callbacks. Is there a reason why you could not?

@domenic

This comment has been minimized.

Copy link
Member

domenic commented Dec 13, 2017

I think @annevk is remembering an async-ified version, perhaps based on async iterators. But observables are much closer to (really, isomorphic to) the EventTarget model already, so don't have this problem. In particular, they can call their next callback in the same turn as the event was triggered, so e.preventDefault() will work fine.

It really is just A Better addEventListener (TM). ^_^

@annevk

This comment has been minimized.

Copy link
Member

annevk commented Dec 13, 2017

(It seems my concern indeed only applies to promise returning methods, such as first().)

@appsforartists

This comment has been minimized.

Copy link

appsforartists commented Dec 13, 2017

@benlesh Can you speak more to why the subscribe(observer) signature isn't valid? That's how all the observable implementations I've seen currently work, but they aren't written in C or Rust.

@domenic

This comment has been minimized.

Copy link
Member

domenic commented Dec 13, 2017

In web APIs, per the rules of Web IDL, it's not possible to distinguish between a function and a dictionary. (Since functions can have properties too.) So it's just disallowed in all web specs currently. Figuring out how or whether to allow both o.subscribe(fn) and o.subscribe({ next }) is the TODO.

To be clear, the tricky case is

function fn() { console.log("1"); }
fn.next = () => { console.log("2") };

o.subscribe(fn);

Which does this call? Sure, we could make a decision one way or another, but so far in web APIs the decision has been to just disallow this case from ever occurring by not allowing specs that have such overloads, So whatever we do here will need to be a bit novel.

This is all a relatively minor point though, IMO. Perhaps we should move it to https://github.com/heycam/webidl/issues.

@benlesh

This comment has been minimized.

Copy link

benlesh commented Dec 13, 2017

Another thing that would be interesting to know is what the various frameworks and libraries do here and whether this would make their job easier.

I can't speak for frameworks, directly. Perhaps @IgorMinar or @mhevery can jump in for that, but for RxJS's part, whenever anyone goes to build an app using only RxJS and the DOM, one of the most common things they need from RxJS is fromEvent, which this would completely replace. I would also definitely love to see an RxJS that was simply a collection of operators built on top of a native Observable we didn't always have to ship.

@appsforartists

This comment has been minimized.

Copy link

appsforartists commented Dec 13, 2017

Thanks for the clarification.

It's interesting to me that TC39 and WHATWG both have the ability to add JS APIs, but with different constraints. The TC39 proposal decides what to do based on if the first param is callable. If the TC39 proposal was stage 4, the browsers would be implementing that behavior, right? (Or maybe the TC39 proposal was supposed to be in WebIDL too and violated this. I hadn't heard about that, but I'm not a TC39 member either).

@keithamus

This comment has been minimized.

Copy link

keithamus commented Dec 13, 2017

FWIW in practice the DOM already makes the distinction of function-vs-object in the case of addEventListener:

const handle = e => console.log('main!', e)
handle.handleEvent = e => console.log('property!', e)
document.body.addEventListener('click', handle)

// Logs with `main!`

(Not suggesting WebIDL can't make the distinction, just pointing out there is a precedent here)

@jhusain

This comment has been minimized.

Copy link

jhusain commented Dec 13, 2017

@annevk concerns about the ability to call preventDefault() when using Promise returning methods are valid. Mutation use cases could be addressed with a do method which allows side effects to be interleaved.

button.on(“click”).do(e => e.preventDefault()).first()

This method is included in most userland Observable implementations.

@benlesh

This comment has been minimized.

Copy link

benlesh commented Dec 13, 2017

@jhusain it could also be handled with map, although it would drive some purists crazy:

button.on('click').map(e => (e.preventDefault(), e)).first()
@appsforartists

This comment has been minimized.

Copy link

appsforartists commented Dec 13, 2017

The biggest thing I see missing from this proposal is an official way to use userland operators. @benlesh, do you think pipe is ready to be included?

To the question about frameworks/libraries, as the author of a userland implementation, there are three core types that I'm interested in:

  • Observable
  • MemorylessSubject - It's both an Observer and an Observable. Calls to its next method are passed to all the observers who have subscribed.
  • Subject - In addition to being both an Observer and and Observable, it remembers its most recent emission and calls new subscribers with it immediately.

If Observable is standardized without the Subjects, userland libraries will still ship them. Still, any standardization is good for the whole ecosystem - it means operator authors know what interface they must implement to be interoperable with the ecosystem.

Furthermore, if Observable is standardized, I expect it will become common knowledge among web authors (just like promises, generators, etc.) That will make it easier for frameworks to depend on it without having to worry about intimidating their users with a steep learning curve - understanding observables becomes part of the job.

@jhusain

This comment has been minimized.

Copy link

jhusain commented Dec 13, 2017

@benlesh Using map would be less ergonomic for this use case, because developers would be obligated to also return the function argument.

button.on(“click”).map(e => {
  e.preventDefaut():
  return e;
}).first()

The do method would allow preventDefault() to be included in a function expression, and would consequently be more terse. Given how common this use case may be, the do method may be justified.

@johanneslumpe

This comment has been minimized.

Copy link

johanneslumpe commented Dec 13, 2017

@appsforartists Nitpick, but a Subject should not retain its current value. Rather than having a Memoryless subject, I think it should be the other way around: having a Subject that can retain its latest value, like BehaviorSubject in rxjs. Adding functionality on top of the common base, instead of removing it.

@appsforartists

This comment has been minimized.

Copy link

appsforartists commented Dec 13, 2017

Not to bikeshed, but the name of that method seems to keep flipping between do and tap. I don't know the reasoning for the changes, but I suspect tap is easier for users to disambiguate from do blocks and do expressions.

@appsforartists

This comment has been minimized.

Copy link

appsforartists commented Dec 13, 2017

@johanneslumpe Agree that the information architecture is a bit wonky. I think I originally learned Subject has a value, so I think of the other as MemorylessSubject, but that doesn't mean we should standardize that way.

@emilio-martinez

This comment has been minimized.

Copy link

emilio-martinez commented Dec 13, 2017

@jhusain If I may, I believe you're nitpicking a bit too much on that point.

First, the benefits and ergonomics of having the listener removed due to the first() operator are much greater than the possible ergonomics lost on preventDefault(). In fact, it's the ergonomics for adding+removing event listeners which would make this API so rich.

Second, calling preventDefault() would probably not happen quite as you mention in your example. I believe it would be more like

button.on('click').first().map(e =>
  e.preventDefault();
  ... do more stuff with event
})
@benlesh

This comment has been minimized.

Copy link

benlesh commented Dec 13, 2017

@jhusain I completely agree. I was just demonstrating that if there was a concern over method proliferation, it's possible with existing proposed methods. (And I know about the return requirement, notice the sly use of the comma in my example)

@benlesh

This comment has been minimized.

Copy link

benlesh commented Dec 13, 2017

Not to bikeshed, but the name of that method seems to keep flipping between do and tap.

@appsforartists, that's an RxJS implementation thing. Unrelated to this issue.

@TimothyGu

This comment has been minimized.

Copy link
Member

TimothyGu commented Dec 13, 2017

For Node.js to accept (and implement) this, the on() method name must be changed, given that the Node.js EventEmitter class has on() as an alias for addEventListener(). People who program both for Node.js and for the web will just get more confused between the different behaviors on different platforms.

@benlesh

This comment has been minimized.

Copy link

benlesh commented Dec 13, 2017

@appsforartists this proposal is really meant to meet needs around events in the DOM, and happens to ship with a nice, powerful primitive. We should keep it to that and not over-complicate it.

@domenic

This comment has been minimized.

Copy link
Member

domenic commented Dec 13, 2017

@TimothyGu Node could switch on the number of parameters provided to the on() method. That is, assuming Node wants to do direct integration into their EventEmitter like the web platform does.

@hrajchert

This comment has been minimized.

Copy link

hrajchert commented Dec 13, 2017

Congrats all for bringing this proposal 🎉. I just want to address a point that I think the current implementation of RxJs is missing.

Currently, both Promises and Observables can only be typed on the value, not on the error. And I know talking about types (TypeScript or flow) is a strange thing to do in an Observable proposal, but the underlying reason is a subtle one, and the behaviour of how Observables handle different error situations is paramount.

The problem, as stated here, arrise when handling functions that throws errors, for example

Observable
  .of(1)
  .map(_ => {throw 'ups'})
  .fork(
       x => x,
       err => err // The error will be the string 'ups', but the type checker can only type it as any
  );

We can't avoid to handle these types of error as either your functions or functions that you call may throw, but on the other hand this type of error handling goes against of how we should do it functionally. It would be better to use a chainable method like .switchMap and Observable.throw or in the case of promises .then and Promise.reject.

For that reason, both Promises and Observables can only be typed on error as any and sadly it is the correct type. Luckly I think there are at least two possible solutions, one that is relevant to the Observable proposal.

One solution would be to try catch all methods that may throw and wrap the possible error into a class that extends from Error, for example

class UncaughtError extends Error {
}

which would make the following example possible

Observable
  .of(1)
  .map(_ => {throw 'ups'})
  .switchMap(_ => Observable.throw(new DatabaseError())
  .fork(
       x => x,
       err => err // The error type would be UncaughtError | DatabaseError
  );

Note that UncaughtError is always a posibility both if you have a function that throws or not but DatabaseError could be infered from the types of Observable.throw and switchMap.

Very recently I created a Promise like library called Task (WIP) that takes this into account and allow us to type both success and error cases. So far the results where more than satisfying.

The other solution I can think of would be if TypeScript or flow implements typed exceptions, but I don't think is the path their plans.

I hope you take this situations under consideration and thanks for the great work

Cheers!

@TimothyGu

This comment has been minimized.

Copy link
Member

TimothyGu commented Dec 13, 2017

Node could switch on the number of parameters provided to the on() method.

The IDL proposed in the OP allows a second options-style parameter.

But I'd also like to point out jQuery's on() as evidence for that the name simply has too much historical burden to it.

@keithamus

This comment has been minimized.

Copy link

keithamus commented Dec 13, 2017

At the risk starting the bikeshedding wars around the API; would .observe() be a reasonable name for this?

@benlesh

This comment has been minimized.

Copy link

benlesh commented Dec 13, 2017

At the risk starting the bikeshedding wars around the API; would .observe() be a reasonable name for this?

@keithamus Certainly! But there's already a huge amount of industry momentum around subscribe as the primary API here. There's also some interesting nuance around what this method does. In all cases, it does observe, but in many cases it not only observes, but it also sets up the production of values. In this particular use case (EventTarget), it's only really observing, so the name "observe" makes total sense, but we would be sacrificing a name that makes a little more sense for other use cases of the Observable type as a primitive.

@TimothyGu

This comment has been minimized.

Copy link
Member

TimothyGu commented Dec 13, 2017

To be clear, anything other than on would be fine with me. I’ll leave y’all to determine what’s best other than that :)

@jhusain

This comment has been minimized.

Copy link

jhusain commented Dec 15, 2017

It’s likely that we can’t provide APIs which allow Observables to be adapted into Promises without introducing footguns. Observable and Promise have very different semantics around dispatch (for good reasons), which creates impedance mismatches.

Champions believe that there are compelling use cases to adapt Observables to Promises, and developers will likely do this anyway. However there is an argument to be made that making adaptation more arduous increases the likelihood that it will only be used when necessary. For example, removing first() will increase the likelihood that developers don’t cavalierly adapt to a Promise instead of using obs.take(1).subscribe(...) when the latter is more appropriate. If we accept this argument, we have the following options:

  1. Remove Promise-returning methods from the Observable prototype.
  2. Modify the Promise-returning methods to return Observables which notify only once.

Personally I favor the second. Developers are already used to the idea of subscriptions which notify once (ie { once: true }). However I think there are valid arguments to be made that developers might be confused why functions that return scalars values on Array return a vector type on an Observable. This concern has been expressed when the option was raised in the past.

@jakearchibald Note that making the Observable returned by first awaitable is still a footgun, because scheduling will still happen on await.

@benlesh

This comment has been minimized.

Copy link

benlesh commented Dec 15, 2017

@benlesh that misses the case where the signal has already aborted, but I guess that's easy to work around?

@jakearchibald In that case you could either synchronously check the signal prior to subscription, or write a simple adapter around it using the Observable constructor. If you're interested in what what might look like, let me know.

Note that making the Observable returned by first awaitable is still a footgun

@jhusain is correct here. Because Promises are always eager and Observables will lazily start observation, Observables can have side-effects. Which would mean that calling a "then" directly on an Observable would mean creating side-effects, when in Promise's case it would never do that. It would be very surprising behavior for developers.

Modify the Promise-returning methods to return Observables which notify only once.

Personally, I like this idea as it already exists in this form in RxJS and I know it works out for most use cases. I still think we should have a more obvious method like toPromise as well though, because I really believe we should try to provide interop points where we can. It makes the type more powerful.

@benlesh benlesh closed this Dec 15, 2017

@benlesh benlesh reopened this Dec 15, 2017

@benlesh

This comment has been minimized.

Copy link

benlesh commented Dec 15, 2017

Oops... accidental close, just missed with the mouse, sorry.

@bmeurer

This comment has been minimized.

Copy link

bmeurer commented Dec 18, 2017

I'm definitely not an expert here. My gut feeling is that adding primitives by introducing new browser APIs might be a challenge performance wise later and might lead to (potentially unnecessary) copy&paste on the Node side later. Would it make sense to resurrect the idea of providing Observable primitives on the VM layer and have browser and Node specify APIs in terms of those primitives? Much like with Promises?

@mathiasbynens

This comment has been minimized.

Copy link
Member

mathiasbynens commented Dec 18, 2017

@bmeurer #544 (comment) explains it somewhat. I would love to hear more about this as well, but I do feel like the discussion on where to spec this should be moved to a separate thread somewhere.

@YurySolovyov

This comment has been minimized.

Copy link

YurySolovyov commented Dec 18, 2017

@mathiasbynens not sure if that was brought up, but strictly speaking, it does not have to be syntax-driven feature, just adding new global/API to spec might work fine too, like with Promise

@bmeurer

This comment has been minimized.

Copy link

bmeurer commented Dec 18, 2017

@mathiasbynens I'm aware of that, and I remember some discussions in the V8 team whether or not it makes sense to have Observables as primitives, similar to Promises.

I'm not speaking as a V8 representative or some TC39 implementor here. Just my own personal opinion:

When we say that Observables are perfectly fine as a user land feature and have a single, canonical implementation of that primitive, i.e. RxJS, that makes total sense to me. However adding Observables as primitives to the platform via some embedder API (be it something in the browser or something in node, or both in the end) doesn't seem like it's the best approach long-term, as you might end up with a lot of copy and paste, and due to performance constraints you'll probably want to have the core part implemented inside the JavaScript engine eventually, which is not covered by the TC39 specification then.

So my question is really: If we think it is useful as a native platform feature, shouldn't the primitive then end up in ECMAScript, and have platform APIs (node/browser) define more complex logic in terms of the primitive? Like we did with Promises? What makes Observables as a primitive different from Promises in this regard?

@domenic

This comment has been minimized.

Copy link
Member

domenic commented Dec 18, 2017

@bmeurer the difference between the two cases was discussed upthread, as @mathiasbynens links to.

@annevk

This comment has been minimized.

Copy link
Member

annevk commented Dec 18, 2017

@bmeurer there's already precedent for features defined outside TC39 ending up in the JavaScript engine, e.g., https://streams.spec.whatwg.org/. I don't think that has resulted in a lot of copy-and-paste.

@othermaciej

This comment has been minimized.

Copy link

othermaciej commented Dec 18, 2017

One difference (though not sure if this is relevant): Promises require special platform hooks to implement correctly due to the timing requirements on resolving a promise. But Observables don't. They would have almost exactly the same behavior whether implemented in the JS engine, in the browser engine (or other JS hosting environment), or in pure JS.

(To be clear: I have no particular opinion on whether they should be specified in an ES-like way or a DOM-like way, what venue this should be done in, or if they should be standardized at all.)

@othermaciej

This comment has been minimized.

Copy link

othermaciej commented Dec 18, 2017

It's time for another rookie question. Someone posted a link to a 2x2 chart like this:

             spatial   temporal
scalar    value     Promise
vector   Array       ???

Where Observable would fill in the missing concept. Why isn't ReadableStream the missing concept? I can see many differences in details of the API, but what is the difference conceptually between a Stream and an Observable?

@benlesh

This comment has been minimized.

Copy link

benlesh commented Dec 18, 2017

@othermaciej The primary difference would be that ReadableStream, AsyncIterator, et al, are pull-based, where Promise and Observable are push-based. Certainly ReadableStream and AsyncIterator would fall into that generic quadrant, but it doesn't mean they can be used in the same way. I'd almost argue that Promise misses the mark in the "temporal" space, because it isn't able to act synchronously, only asynchronously, therefor not all timings could work with it as a primitive. Observables, on the other hand, are much more primitive and do not prescribe any scheduling for values emissions. ReadableStream and AsyncIterator are implemented on top of promise, meaning that they're also tied to the same scheduling restrictions. Restrictions that make them not viable as an abstraction for eventing, as events can be subscribed to, dispatched and torn down synchronously.

@bmeurer

This comment has been minimized.

Copy link

bmeurer commented Dec 18, 2017

@annevk Streams didn't end up in the JavaScript engine. Intuitively I'd have used that as a counter example.

@domenic I probably misread that comment. Let me ask instead: Once you have hooked up Observables to the DOM, would you go back to TC39 and propose the primitive (based on the experience with DOM) to JavaScript again?

@benlesh

This comment has been minimized.

Copy link

benlesh commented Dec 18, 2017

Streams didn't end up in the JavaScript engine. Intuitively I'd have used that as a counter example.

This is likely because of AsyncIterator, which is basically the same thing (pulling promises out of an API), only more primitive. (I'm speculating)

@domenic

This comment has been minimized.

Copy link
Member

domenic commented Dec 18, 2017

It's time for another rookie question. Someone posted a link to a 2x2 chart like this:

For more detail, see https://stackoverflow.com/questions/39439653/events-vs-streams-vs-observables-vs-async-iterators/47214496#47214496

Streams didn't end up in the JavaScript engine.

They did in Gecko and, I believe, WebKit. They arguably did in Blink, although via the abstraction that is V8 extras, which you might not count.

would you go back to TC39 and propose the primitive (based on the experience with DOM) to JavaScript again?

I don't see any motivation for doing this, perhaps because I don't view them as a primitive.

@othermaciej

This comment has been minimized.

Copy link

othermaciej commented Dec 18, 2017

They did in Gecko and, I believe, WebKit. They arguably did in Blink, although via the abstraction that is V8 extras, which you might not count.

WebKit implements Streams at the browser engine (WebCore) level, not the JS engine (JavaScriptCore) level. We do try to make them look more like JS builtins than other bindings do, though perhaps not with full fidelity. This is in part because the having an ECMA add-on spec is unusual and mildly confusing; and in part because nothing else is implemented as a DOM binding that's supposed to look like a JS builtin.

@bzbarsky

This comment has been minimized.

Copy link

bzbarsky commented Jan 29, 2018

I have been trying to understand the proposed semantics, and I admit to being a bit lost. https://github.com/tc39/proposal-observable doesn't seem to really describe them, nor does this issue. Some specific questions I have even without understanding the whole proposal:

  1. There is some sort of queuing happening in an Observable that is not being observed, right? Is this limited in any way? If I do on("mousemove") and then never subscribe to it, have I just created an unbounded memory leak?
  2. When do the observables created by on get created? For example, if I on("DOMContentLoaded") after that event has fired, will I get that notification?
  3. Does on create a new observable each time?

All this would be helped by a processing model description. If there is one already, a link to it somewhere near the beginning of this thread would be much appreciated.

@benlesh

This comment has been minimized.

Copy link

benlesh commented Jan 29, 2018

@bzbarsky

There is some sort of queuing happening in an Observable that is not being observed, right? Is this limited in any way? If I do on("mousemove") and then never subscribe to it, have I just created an unbounded memory leak?

No, there is no queuing at all. If you don't subscribe to the observable returned by on('mousemove') it will effectively be the same thing as not adding an event listener in the first place. Observables do nothing until they are subscribed to.

When do the observables created by on get created? For example, if I on("DOMContentLoaded") after that event has fired, will I get that notification?

The observable itself is created at the moment on("DOMContentLoaded") is called. However observation does not start until you subscribe to the observable.

Does on create a new observable each time?

Yes... however since whatever you're observing already exists, every one of them will effectively multicast. Technically, we could have each call to on return the same observable, as there wouldn't be any difference in behavior.

@slikts

This comment has been minimized.

Copy link

slikts commented Oct 22, 2018

Observables would add ergonomics to EventTarget in two ways: having a standard interface that allows reusable abstractions, and adding pull semantics at the stream level. The issue there is that observables are just a stage-1 proposal, so they're not standard and won't be for years at best, and also that stream-level pull semantics are limited compared to value level.

Async iterables are now a standard stream primitive in the language, and they offer value-level pull semantics using promise generation, which integrates nicely with async functions and other language constructs and is familiar to users.

Converting the EventTarget push streams to iterables requires buffering, but there is a standard solution for that in Streams API, or even a simplified last-value cache could be used (which removes the risk of leaking with the tradeoff of having to be consumed serially).

The workaround to use Event.preventDefault() is straightforwardly just using the current API to add a separate listener. It's not good to have a special case, but it's also arguably the minority of use cases that need it.

Instead of waiting for observables for years, async iteration can be supported now. It'd enable a pattern like this:

for await (const event of element.addEventListener('click')) {
  console.log(event);
}

Calling @@asyncIterator() would add the listener, and the standard return() and throw() hooks can be used to remove the listener.

@jakearchibald

This comment has been minimized.

Copy link
Collaborator

jakearchibald commented Oct 22, 2018

The issue there is that observables are just a stage-1 proposal

See the OP - this is about speccing observables in DOM, not in TC39.

but there is a standard solution for that in Streams API

See #544 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment