WICG / observable

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

Is it possible to optionally "trampoline" subscriptions to be asynchronous? #74

Closed mmocny closed 1 month ago

mmocny commented 1 year ago

My understanding is that .subscription callbacks are always dispatched synchronously as new data arrives, even when the data itself can arrive as a stream, asynchronously.

This is described as a feature and the primary use case was specifically for EventTarget support.

However, I also see in this example a (hypothetical?) alternative to .subscribe using .then syntax to compare Promise behaviour which would queue a microtask instead. My read is that .then isn't actually part of the Observable API proposal and that was just hypothetical-- is that correct?


All that makes sense to me-- however, I think there do exist very real use cases (see at bottom) for desiring that the dispatch of subscription calls would be truly asynchronous (i.e. a new macrotask not just microtask), and especially so for EventTarget.

Perhaps one way to accomplish this manually would be to:

observable.subscribe({
  next: async function() {
    await scheduler.yield();
    // continue...
  }
});

I am not sure if next: can accept and async function... but probably we don't even need to return the Promise since I doubt .subscribe will await it anyway?

Or perhaps there could be a helper to automate this, something like:

element.on('click').asyncify().subscribe({ ... })

Is this already an established pattern that exists?

However, I feel it may be enough important for EventTarget to warrant a subscription helper that is async-dispatch-by-default. Perhaps something like:

// This would call callback in a distinct macrotask... ideally after-next-paint when needed.
element.after('click').subscribe({ ... });

...and perhaps Task priority is also related. EventDispatch is typically very high priority, but perhaps observer callbacks should have the option to change their own priority?


The use case for requesting async dispatch of EventTarget subscription is for decoupling necessary effects which follow important interactions, from unnecessary effects which can be delayed until after next paint.

See the Optimize INP guide for examples of explicitly using yield points to manually to accomplish such patterns.

Today, the web platform does not support native "passive" event listeners which would dispatch callbacks asynchronously (perhaps it could) -- but I wondered if the Observer API already has solutions that would easily address this use case?

mmocny commented 1 year ago

I just noticed this note:

https://github.com/WICG/observable/blob/8d43ff45e9cb63a617377d3e6b4a8c90ff6af911/README.md?plain=1#L533-L536

In this case, I of course am asking for an opt-in mechanism, but it is interesting that this is already somewhat of an existing mechanism.

mmocny commented 1 year ago

Somewhat off-topic but I also found this comment to suggest that it has indeed been common in the observable space generally to follow such a pattern

common case of awaiting each value from the observable, but processing them asynchronously

Though I am not sure if there exists a way to pipe + concatMap + sleep in the current proposal (I guess userland extensions would provide that?)

mmocny commented 12 months ago

After some more reading of RxJS, I believe I am specifically asking for asyncScheduler as part of the larger scheduler feature set.

I suspect this is beyond scope of the proposal at the moment, but seems easy enough to polyfill by creating a custom operator (coincidentally, that example already does what I ask for).

domfarolino commented 1 month ago

However, I also see in this example a (hypothetical?) alternative to .subscribe using .then syntax to compare Promise behaviour which would queue a microtask instead. My read is that .then isn't actually part of the Observable API proposal and that was just hypothetical-- is that correct?

Just to be clear, you are correct that this proposal does not introduce a then() method to any object it doesn't already exist on. However, this proposal does introduce promise-returning methods (to the Observable API), which are of course thenable. So nothing in that example is actually hypothetical.

I've heard some interest in making event handler dispatching optionally asynchronous elsewhere, outside of this proposal, so I'm wondering if this should be something filed more generally against whatwg/dom? Maybe it could be an option passed into addEventListener() via https://dom.spec.whatwg.org/#dictdef-addeventlisteneroptions, and then we could extend https://wicg.github.io/observable/#dictdef-observableeventlisteneroptions to include it? That seems like the cleanest, most holistic route instead of just adding internals that are specific to Observables.

Do you know if there's been any appetite for this among folks you work with to pursue a general DOM Standard event listening proposal for this?

domfarolino commented 1 month ago

I suspect this is beyond scope of the proposal at the moment, but seems easy enough to polyfill by creating a custom operator (coincidentally, that example already does what I ask for).

I think that's right. I think I'd be more comfortable with a more generalized event listening + scheduling API proposal instead of trying to narrowly achieve this with the Observable API, off the back of limited microtask infrastructure that we use for the handful of Promise-returning APIs. Given that, I think I will close this, but if nobody ends up filing a DOM issue for the scheduling primitive + event handling ideas, then I might take this upon myself.

mmocny commented 1 month ago

I think I'd be more comfortable with a more generalized event listening + scheduling API proposal instead of trying to narrowly achieve this with the Observable API

This makes sense to me. I was just curious if Observables will already offer a generic mechanism to, effectively, "yield" before dispatch of callbacks using a simple syntax, rather than relying on the functionality of the .on observable specifically.

Do you know if there's been any appetite for this among folks you work with to pursue a general DOM Standard event listening proposal for this?

I think there is a strong appetite for this. I've been assuming this would just entail extending the "passive" concept to more/all event types. I think developers should be able to register either non-passive and passive listeners and dispatch from discrete tasks, the latter could be delayed until after-next-paint depending on scheduler.

if nobody ends up filing a DOM issue for the scheduling primitive + event handling ideas, then I might take this upon myself.

That would be excellent, and appreciated! I don't myself feel able to carry this task. You likely have more context for some other related projects as well.


FWIW, I also think there is appetite for other, slightly related features, in case these interest you also:

domfarolino commented 1 month ago

I've been assuming this would just entail extending the "passive" concept to more/all event types. I think developers should be able to register either non-passive and passive listeners and dispatch from discrete tasks, the latter could be delayed until after-next-paint depending on scheduler.

Hmm I see what you're saying here, but passive event listeners aren't quite the same thing as the "async" we're looking for here. Passive event listeners allow scrolling (associated with the event) to run in the background. So in a way, the scrolling happens "asynchronously" (in a background thread I guess) with respect to the event dispatching, but the event handler doesn't get fired asynchronously with respect to the platform's creation of the event itself. So I think we want something novel here.

That would be excellent, and appreciated! I don't myself feel able to carry this task. You likely have more context for some other related projects as well.

Alright I filed https://github.com/whatwg/dom/issues/1308!

mmocny commented 1 month ago

passive event listeners aren't quite the same thing as the "async" we're looking for here

Hmmm, I guess there are two distinct concepts and I wonder if we are both describing the same one?

  1. Separating the work needed "before-next-paint" (i.e. which is required to supply the direct functionality of the action itself), from any work that can be delayed "after-next-paint" (i.e. merely wants to observe that the event happened).
  2. Allowing event handlers to dispatch in distinct tasks (technically could want this feature from either of the stages above).

I think passive mode matches (1) while Observables' traditional use of async scheduling would better match (2).


In my 2 cents, the fundamental property of passive events is that the primary / "default action" is handled first, and then later (async) the passive event is fired. Sure, the dispatch of those events is synchronous once that passive event is created-- and it likely would be here for this proposed feature, too.

The main criteria is that you lose the ability to preventDefault and won't be guaranteed to dispatch in the same animation frame as the first visual feedback for the effect.

Today, it's true that this is only useful for scrolling specifically, and that is only because that default action is handled by the browser directly from the compositor.

But I don't think its that different to say that e.g. a click might have default actions like a link click or form submit or just applying css effects from pseudo classes, or a developer might provide custom actions in a non-passive event listener. A passive version of that event would be async, would not support preventDefault, and would not be guaranteed to dispatch in the same animation frame that includes the visual result of the interaction.

The concept seems fairly equivalent to me-- though perhaps too many developers associate the "passive" concept specifically with scrolling. I'm not sure.

mmocny commented 1 month ago

Also I'm not sure if it helps, but, perhaps observers like IntersectionObserver dispatch is another analogy to look at?

mmocny commented 1 month ago

Reading whatwg/dom/issues/1308, {priority} seems like a great alternative suggestion! Today all events (in chromium) start at even greater than user-blocking priority, so any explicit priority would (I think) effectively mean making them passive, as long as you allow decoupling lower-priority event listeners from their higher-priority dispatch.

I'll stop discussion on this thread and move conversation there.