Skip to content
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

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

Closed
mmocny opened this issue Sep 29, 2023 · 10 comments
Closed

Comments

@mmocny
Copy link

mmocny commented Sep 29, 2023

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
Copy link
Author

mmocny commented Sep 29, 2023

I just noticed this note:

observable/README.md

Lines 533 to 536 in 8d43ff4

Note that the operators `every()`, `find()`, `some()`, and `reduce()` return
Promises whose scheduling differs from that of Observables, which sometimes
means event handlers that call `e.preventDefault()` will run too late. See the
[Concerns](#concerns) section which goes into more detail.

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
Copy link
Author

mmocny commented Sep 29, 2023

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
Copy link
Author

mmocny commented Oct 8, 2023

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
Copy link
Collaborator

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
Copy link
Collaborator

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.

@domfarolino domfarolino closed this as not planned Won't fix, can't repro, duplicate, stale Aug 13, 2024
@mmocny
Copy link
Author

mmocny commented Aug 14, 2024

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:

  • lazily initialized event listeners
    • something like: event replay after async fetch to bootstrap a controller
    • Today this is typically done by libraries/frameworks which require an early bootstrap script and annotations in the served html
    • ...that works, but various web platform features are blind to it, and don't consider the eventual replay as an actual event: we don't measure event timing performance, we don't toggle transient user activation, etc.
  • something like blocking=rendering but for blocking=interactions
    • for delaying dispatch of very early interactions while page is still loading
    • event dispatch is typically high priority and might out-race even ready async/defer scripts.
    • scheduling of hit testing and dispatch is already racy and some throttling during early load already exists afaik.

@domfarolino
Copy link
Collaborator

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 whatwg/dom#1308!

@mmocny
Copy link
Author

mmocny commented Aug 27, 2024

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
Copy link
Author

mmocny commented Aug 27, 2024

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

@mmocny
Copy link
Author

mmocny commented Aug 27, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants