WICG / observable

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

Semantics of `do` (aka `tap` in many impls) #111

Open benlesh opened 4 months ago

benlesh commented 4 months ago

do is a simple but important method for Observable. It allows for repeatable side effects, and is commonly used for logging etc.

An example use case would be if someone were to create some sort of shared connection observable and pass it around to users, such as a component that may be mounted more than once, but they want to log what's going through it.

/** connection.js */

// Underlying impl could be anything. Websockets, http/2 streaming, SSE.
const sourceStream = getSharedStreamData('someurl.com/whatever');

export const connection = sourceStream
// Use `do` to add logging to an observable
// we might subscribe to many times.
.do({
  subscribe: () => {
    console.info('stream connected');
  },
  next: (value) => {
    console.debug(`stream next`, value);
  },
  error: (error) => {
    console.error(error);
  },
  complete: () => {
    console.info('stream complete: disconnected by source'),
  },
  abort: () => {
    console.info('stream disconnected by user')
  },
})
/* consumer-component.js */

import { connection } from './connection.js';

export ConsumerComponent extends HTMLElement {
  constructor() {
    this.controller = new AbortController();
  }

  connectedCallback() {
    connection.subscribe(
      (update) => this.handleStreamingUpdate(update),
      { signal: this.controller.signal }
    );
  }

  disconnectedCallback() {
    this.controller.abort();
  }

  handleStreamingUpdate(update) {
    // TODO: update the view or something.
  }
}

Other information

For a VERY LONG TIME, RxJS only mirrored what you could do in subscribe in do (aka tap in RxJS). However, there were many valid requests to be able to use do to inspect when you subscribe or unsubscribe. This isn't necessary at subscribe because you know when you subscribe when you actually call subscribe. Similarly the abort (unsubscribe in RxJS) callback is not required when the consumer aborts, because the consumer knows when it aborts, it just did it. It owns that code.

This method is also one of the better ways to do debug logging so you can see what is happening over time, and is in popular use there. I'm actually surprised that Promise doesn't have a do. Without do, most people will just use source.map(x => (/* side effect */, x)), which is limited, not very semantic, and inefficient.

ljharb commented 4 months ago

(fwiw i think tap is a much more intuitive and understandable name than do, given that do is a language keyword)

benlesh commented 4 months ago

@ljharb: Interesting. RxJS originally had do, and I think I pulled tap out of my butt (or someone else's) because do was a keyword. I'm indifferent on the name, TBH. If everyone likes tap, that's less education for folks using RxJS, I suppose.

bakkot commented 4 months ago

Some previous discussion in https://github.com/WICG/observable/issues/29

bakkot commented 4 months ago

(On the subject of naming, Rust spells this .inspect.)

domfarolino commented 3 months ago

I also like tap and think it got some good reception in #29 as @bakkot points out, and it's especially promising that it might have a place in iterator helpers some day, under that name, given https://github.com/WICG/observable/issues/29#issuecomment-1656430426. With that, I think I'll move forward spec'ing tap() as proposed here.

bakkot commented 3 months ago

I would maybe call it inspect, following Rust? That seems like a more obvious name. But either's ok.

domfarolino commented 3 months ago

That might be even better, especially since there is some language precedent with it.