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

Example APIs that could leverage Observables #72

Open
benlesh opened this issue Sep 26, 2023 · 10 comments
Open

Example APIs that could leverage Observables #72

benlesh opened this issue Sep 26, 2023 · 10 comments
Labels
possible future enhancement An enhancement that doesn't block standardization or shipping

Comments

@benlesh
Copy link
Collaborator

benlesh commented Sep 26, 2023

@domenic suggested it might be good to track platform APIs for which observables would work well.

A few that come to the top of mind for me:

  1. interval(number): Observable<number> - A setInterval, and you'd get either a counter or a high-precision timestamp in it (similar to what requestAnimationFrame does).
  2. animationFrames(): Observable<number> - basically an animation loop
  3. timer(number): Observable<void> - just setTimeout. Useful for takeUntil, creating timeouts, and many other things. Arguably interval(number).take(1) is the same though. RxJS's implementation also allows a Date to be passed to fire the timer at a specific date/time (although it has some limitations and I don't think it's used very often).
@zloirock
Copy link

zloirock commented Sep 26, 2023

Rather timer(number): Promise<void> - it's not a case for Observable.

@domfarolino domfarolino added the possible future enhancement An enhancement that doesn't block standardization or shipping label Apr 3, 2024
@domfarolino
Copy link
Collaborator

domfarolino commented Apr 3, 2024

The native observers are probably good candidates for this too: MutationObserver, IntersectionObserver, ResizeObserver. All of these could vend Observables and the many operators on the Observable API would probably be super useful there as well. In fact this is something @smaug---- briefly brought up at TPAC 2023 as good possible integration points for this API in the future.

@keithamus
Copy link
Collaborator

MutationObserver, IntersectionObserver, ResizeObserver, PerformanceObserver, PressureObserver, ReportingObserver, are all good candidates but need some thought about the API. Prompted by https://x.com/domfarolino/status/1815474679513276641 I'll list my thoughts here instead:

All constructors take a callback which recieves the respective records, but in the case of an Observable they'd be the next() value, which causes a conflict. This means we either:

  • Put the [bikeshedMeReturnAnObservable]() method on the prototype, and either:
    • never call the constructor callback.
    • make it optional, maybe error if a callback is supplied and [bikeshedMeReturnAnObservable]() is called.
    • call the callback each time alongside next(), which is maybe the weirdest.
  • Make [bikeshedMeReturnAnObservable]() a static method on each of the observer objects.
  • Attaching methods on Node.prototype, e.g. Node#observeResizes()/observeMutations()/observeIntersections(). This makes sense as the *Observer objects themsevles are inert until they get "attached" to a node.

The callbacks always return an Array of entries, batched to intervals. Maybe it makes sense for observables to only recieve one at a time?

Lastly, I think a big concern with these observers is that takeRecords() is quite an important aspect of some of these APIs to ensure records are collected, for example when trying to tear down one of the observers without missing records. I'm not sure the best way to represent that. Subclassing Observable/Subscriber seems excessive for this?

@domenic
Copy link
Collaborator

domenic commented Jul 24, 2024

This means we either:

I like the latter two options you list here (static method, possibly named observe(), or observeResizes() etc.). The first one is too awkward in my opinion. An XObserver with no callback is basically storing no state so it doesn't really make sense to use instance methods.

@domenic
Copy link
Collaborator

domenic commented Jul 24, 2024

Subclassing Observable/Subscriber seems excessive for this?

I think it might work well. Unlike promises, where subclassing is a huge mess because we made their internals so exposed, subclassing for Observable might be more realistic.

Someone would need to work out the details though. Probably worth a separate issue.

@esprehn
Copy link

esprehn commented Sep 18, 2024

The DOM Observer style APIs were specifically designed to allow the browser to efficiently queue and filter records. I don't think Observable makes sense there because it it encourages a single observer for every element (or observation) and doing userland filtering instead of browser internal filtering.

@esprehn
Copy link

esprehn commented Sep 18, 2024

Also the intent was for setTimeout, setInterval and requestAnimationFrame APIs to all be replaced by https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/postTask which can handle all those situations and supports abort signal and dynamic priorities.

I don't think another set of APIs should be added that duplicates the scheduler API, though the scheduler API could be evolved (ex. to have a version that doesn't use a callback).

@benlesh
Copy link
Collaborator Author

benlesh commented Sep 20, 2024

Sort of a shame, postTask isn't very ergonomic if you need to cancel the task though, because it's promise-based. So you're forced to handle a rejection or get noisy error logs.

In any case, it probably couldn't replace setInterval with that API. setInterval will schedule tasks to run at specific moments, trying to limit "drift". You get "drift" when you try to use something like setTimeout (or postTask) to recursively schedule an interval.

For example, this code will slowly drift off of emitting once a second because of how long the work takes between scheduling... Even if you move the run() before the callback(), it will drift just from the overhead of scheduling each task alone. Which is one of the reasons why setInterval exists.

function recursiveInterval(callback, ms) {
  let id = 0;

  const run = () => {
    id = setTimeout(() => {
      callback();
      run();
    }, ms);
  }
  
  return () => clearTimeout(id);
}

recursiveInterval(() => {
  // do some work.
  for (let i = 0; i < 1e7; i++) {}
  
  console.log(new Date().toString())
}, 1000)

@benlesh
Copy link
Collaborator Author

benlesh commented Sep 20, 2024

For requestAnimationFrame, I'd say Scheduler.animationFrames would make an excellent observable API for scheduling an animation loop.

const startTime = performance.now();
Scheduler.animationFrames
  .map((now) => now - startTime)
  .subscribe((elapsedTime) => {
    // move something based on elapsed time.
  });

For setInterval, maybe Scheduler.interval(ms: number) would be a good observable API.

const thirtyMinutes = 30 * 60 * 1000;

const pollingInterval = Scheduler.interval(thirtyMinutes);

pollingInterval.flatMap(async function* () {
  const response = await fetch('/get/data');
  
  if (response.ok) {
    yield response.json()
  } else {
    console.error('Error polling for data');
  }
})
.subscribe(results => {
  updateSomeList(results);
});

@esprehn
Copy link

esprehn commented Sep 20, 2024

In your example you're not immediately scheduling the next iteration. setTimeout is async, call it immediately upon entering run instead. That's all the browser does inside setInterval too, it immediately schedules the next task (with offset adjustment) before running the callback.

function recursiveInterval(callback, ms) {
  let id = 0;

  const run = () => {
    id = setTimeout(() => {
      run();
      callback();
    }, ms);
  }
  
  return () => clearTimeout(id);
}

That'll reduce the drift considerably. Note that browsers already do firing alignment on timers to conserve power, and timers will also skew because of event loop business, but it is true that browsers try to align the to the requested repeat interval:

https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/timer.cc;l=155;drc=48ee6c4ee320c1bcc4f7d01d5c293e6d41ecf648

you could do that yourself and get identical behavior to the browser, but I wouldn't expect folks to figure that out. You gave that feedback over here:

WICG/scheduling-apis#8 (comment)

Hopefully Scott will add repeat to the scheduler.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
possible future enhancement An enhancement that doesn't block standardization or shipping
Projects
None yet
Development

No branches or pull requests

6 participants