WICG / observable

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

Do we add `switchMap`? #90

Closed benlesh closed 6 months ago

benlesh commented 6 months ago

switchMap was mentioned at TPAC as being an API of interest. It's a method that is especially useful for composing events in the browser because of its inherent cancellation. If API bloat is a concern, I would gladly remove toArray(), some(), find() et al, as those are minimally useful with observables.

Use cases are things like:

Use Case: Lookahead Search

In the below scenario, the result of the fetch would simply be ignored.

It's plausible that we could provide a signal along with the switchMap callback, as well. In fact, I'd almost encourage that, as it might make fetch more useful. But I think we'd want to discuss the ergonomics there, and that could be debated and added later. (For example, we might want to have it automatically swallow DOMExceptions named AbortError?)

const input = document.getElementById('search');
const resultList = document.getElementById('result-list');

input.on('input')
  .switchMap(async (e) => {
    const response = await fetch(`/search?q={e.target.value}`);

    if (!response.ok) {
      console.warn(`Search response error: ${response.status} ${response.statusText}`
      return;
    }

    return response.json();
  })
  .subscribe((results) => {
    resultList.innerHTML = results
      .map(result => `<li><a href="${result.href}">${result.text}</a></li>`)
      .join('')
  })

Use Case: Changing Connections

Below, every time a URL is changed in an URL input, if it's a valid URL, it will disconnect from the previous web socket and connect to a new one.

function streamData(url) {
  return new Observable(subscriber => {
    const ws = new WebSocket(url);
    ws.onmessage = e => subscriber.next(e);
    subscriber.addTeardown(() => ws.close());
  })
}

const urlInput = document.getElementById('url-input');

urlInput.on('input')
  .filter(e => e.target.checkValidity())
  .switchMap(e => streamData(e.target.value))
  .subscribe(console.log);
benlesh commented 6 months ago

cc @domfarolino @domenic

bakkot commented 6 months ago

Previously: https://github.com/WICG/observable/issues/52

Your first example kind of scares me as written - the right thing is to cancel the fetch, and I wouldn't want to add a convenience method which makes the wrong thing much easier than the right thing. So I would definitely want to explore avenues for making abort-on-unsubscribe patterns easier, if we were to add this.

One wild idea for that: the mapper (in this and potentially other callback-taking methods) could take as its third argument a { signal }, created by and internal to the operator, so that you could do

input.on('input')
  .switchMap(async (e, idx, { signal }) => {
    const response = await fetch(`/search?q={e.target.value}`, { signal });
    /* ... */
  })
  .subscribe((results) => { /* ... */ })

and have the signal automatically abort when the observable returned by the callback is unsubscribed. (Similar idea suggested at https://github.com/tc39/proposal-iterator-helpers/issues/162#issuecomment-968929171, though I think it's more relevant to switchMap than map etc because unsubscription of the inner thing can happen prior to unsubscription of the outer thing.)

benlesh commented 6 months ago

Previously: https://github.com/WICG/observable/issues/52

Oops. 😬

benlesh commented 6 months ago

I'll move this over to the other pre-existing thread.