WICG / observable

Observable API proposal
https://wicg.github.io/observable/
Other
569 stars 13 forks source link

`on()` collides with all sorts of code in the wild. #39

Closed bmeck closed 1 month ago

bmeck commented 1 year ago

Lots of libraries and runtimes use on() as a generic event handling mechanism. This name might cause problems for them updating and as such cause unnecessary friction. It would be good to see if there is another name that is less prevalent in the same design space that wouldn't collide with Cloudflare Workers, Bun, Node.js, etc. and the libraries written for them (Note: all of these have some form of EventTarget, but often mix with EventEmitter's on in the wild or for backwards compatibility).

domfarolino commented 1 month ago

Update, I've put an X poll out for this: https://x.com/domfarolino/status/1815458583334842691 to get a feel for how the community feels.

benlesh commented 1 month ago

I'm partial to when. I don't like verbs like listen or observe because they sound like they start the subscription, although observe isn't that bad.

I'd consider how it reads in composition like:

element.when('mousedown')
  .switchMap(start => document.when('mousemove')
    .map(current => [
      current.clientX - start.clientX,
      current.clientY - start.clientY,
    ])
    .takeUntil(document.when('mouseup'))
  .subscribe()
tetsuharuohzeki commented 1 month ago

Update, I've put an X poll out for this: https://x.com/domfarolino/status/1815458583334842691 to get a feel for how the community feels.

I would like to vote .listen() in that reply.

Jamesernator commented 1 month ago

I don't like verbs like listen or observe because they sound like they start the subscription

I have the same feeling, but I feel like a noun like .events(...) or similar makes more sense.

benlesh commented 1 month ago

I'd still push on on, and keep when in our back pocket though.

domfarolino commented 1 month ago

Sadly I think we have to call time of death on on() — I don't think the conflict is something we can overcome.

In the poll .observe() is winning, but I'm sympathetic to the concern of using a verb (like .observe()) which sounds very active, even though it just returns a "lazy" Observable.

Now I'm trying to see if the thoughts in https://github.com/WICG/observable/issues/72 will influence our decision at all. If we give *Observer APIs the ability to vend Observables in the future via an overloaded .observe() method, then does that mean we should also go with .observe() for EventTarget? Or is EventTarget special for some reason.

domenic commented 1 month ago

Now I'm trying to see if the thoughts in #72 will influence our decision at all. If we give *Observer APIs the ability to vend Observables in the future via an overloaded .observe() method, then does that mean we should also go with .observe() for EventTarget? Or is EventTarget special for some reason.

I love the holistic thinking here.

Let's make it a bit more concrete. If we used observe() for both with the static method option from #72 then we'd have:

// (1)
imageElement.observe("load").subscribe(...);
ResizeObserver.observe(imageElement).subscribe(...);

If we used observe() for EventTarget with the instance method version from #72:

// (2)
imageElement.observe("load").subscribe(...);
imageElement.observeResizes().subscribe(...);

If we use when() for EventTarget and observe() for the static method option from #72 then we'd have:

// (3)
imageElement.when("load").subscribe(...);
ResizeObserver.observe(imageElement).subscribe(...);

If we use when() for EventTarget and the instance method version from #72:

// (4)
imageElement.when("load").subscribe(...);
imageElement.whenResized().subscribe(...);

Overall I like (2) and (4) the most. It feels like a great unification of events and XObservers, where the XObservers just need slightly more specialized methods instead of the generic event-name-taking method. This also ties into the discussion in #72 about how maybe these methods might need to return subclasses.

(1) is also reasonable, although it might be a bit confusing that the arguments to observe() are so different.

I think (3) feels bad, but concretely wouldn't be the worst thing. It just makes the APIs feel completely separate. Which, to be fair, they kind of are; they just share a return type.

jasnell commented 1 month ago

Of all the alternatives discussed, I'm +1 for when or observe. Of the options presented by @domenic, I'm partial to (2) and (4) also, tho I do also like the symmetry of (1).

domfarolino commented 1 month ago

I thought about this a bit more, and I think the visual symmetry of (1) is misleading. More generally, I think the symmetry of having a pre-existingXObserver#observe() instance method and either EventTarget#observe() instance method or XObserver#observe() static method is misleading.

It means:

  1. Both EventTarget and XObserver have instance methods that share a name but have completely different behavior (one is passive and the other is active)
  2. XObserver itself has two "different" methods with the same name: one static and passive, one instance and active. But both by the same name!

That worries me, and pushes me to option (3) or (4) — when(). Option (4) looks the cleanest, but I'm trying to think through how much of a limitation it is that Element#whenResized() produces an RO Observable only for the element it's called on.

Let's keep going with the ResizeObserver-specific example. Today, when you use ResizeObserver, you create one with a single potentially massive and complicated callback, and then you can call observe() any number of times with different target elements. The same shared callback is used for processing all resizes, for all elements being observed, and presumably there is some (performance?) value to that. But with Element#whenResized() and no work done to ResizeObserver itself, you have to call el.whenResized().subscribe(massiveComplicatedCallback) on many elements, and have potentially many copies of that callback floating around, with many different internal ResizeObservers under the hood. That's a lot less shared infrastructure, but maybe that's OK.

It was out of hope for "sharing more stuff" that I mentioned an Observable-returning method on XObserver that's not static. I envisioned something like new ResizeObserver(massiveCallback).whenResized(element).subscribe(), but hadn't thought it through — I don't think this works at all since the callback would need access to subscriber to plumb values downstream. So I think the only way to do it would be to add a static method on XObserver which is option (3), and doesn't seem all that useful. It takes me to @domenic's comment in https://github.com/WICG/observable/issues/72#issuecomment-2246837210:

An XObserver with no callback is basically storing no state so it doesn't really make sense to use instance methods.

... an argument I might extend, to apply to static methods too. That is, ResizeObserver.whenResized().subscribe(massiveCallback) doesn't seem any more useful than element.whenResized().subscribe(massiveCallback). I guess the only benefit is that shares the same internal ResizeObserver under the hood. (That, and for MutationObserver we get to keep takeRecords() where it is.)

domfarolino commented 1 month ago

All of that is to say that at least on this thread, I think I feel settled on when(). I think we need to spend more time thinking about integration with other APIs in https://github.com/WICG/observable/issues/72, but I think we have enough to go off of here to make the decision on when() in the meantime, regardless of how the integration details look. Thoughts?

triskweline commented 3 weeks ago

Thank you for considering these concerns :heart: