WICG / observable

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

"Observable is a glorified function" is confusing framing #6

Closed domenic closed 1 year ago

domenic commented 1 year ago

Maybe there is some abstract sense in which this is true, but it makes no sense to me. The natural followup questions with this framing are:

And then

Additionally, it has extra "safety" by telling the user exactly when:

just makes it seem like some of the most important features of observables, don't even fit into this model?


I would suggest describing how observables work, independent of any analogy with functions. IIUC the main points to communicate are:

I would then very quickly move into making this concrete via events, possibly with code examples. I.e., you can call subscribe() as many times as you want, and every time, it calls "the creator's code" which subscribes to the EventTarget. (Using the same mechanism, under the hood, as addEventListener() does.) This makes it more clear why the above properties are desirable properties, for practical purposes. (Although you need to do a bit more work to explain the complete and error signals.)

benlesh commented 1 year ago

The context around the statement, step by step:

  1. You can write a function that does something similar:
// An observable-ish function
function ticker(observer) {
  let n = 0;

  const id = setInterval(() => {
    observer.next(n++)
    if (n === 5) {
      observer.complete();

      // developer mistake here
      observer.next('oops')
    }
  }, 1000);

  return () => {
    clearInterval(id);
  }
}

// usage

const unsub = ticker({
  next: console.log,
  complete: () => console.log('done')
})

// unsubscribing later (optional)
setTimeout(() => {
   unsub();
}, 60000);

This seems fine, but unfortunately there are a lot of bugs that aren't immediately obvious to the developer.

  1. There's calling observer.next() after observer.complete(). That shouldn't do anything, and it will.
  2. Worse: When the thing completes, the interval will keep ticking along until the external provider unsubscribes by calling the returned function.

To resolve these issues, we have to take the SAME function and wrap it in something that will internally provide these guarantees:


// It's the same function, but we've wrapped it in a class
// that will take the passed observer and wrap it in a "subscriber"
// that does a few things:
// 1. Ensures safety around calling `next`, `complete`, or `error` after everything is finalized.
// 2. Links the subscription teardown to the calling of `complete` or `error` to guarantee cleanup.
// 3. Allows partial observers to be passed (each handler is optional).
const ticker = new Observable((subscriber) => {
  let n = 0;

  const id = setInterval(() => {
    subscriber.next(n++)
    if (n === 5) {
      subscriber.complete();

      // developer mistake here
      subscriber.next('oops')
    }
  }, 1000);

  return () => {
    clearInterval(id);
  }
});

// usage
// Subscribe just immediately calls the function with the
// subscriber that wraps the passed observer.
const subscription = ticker.subscribe({
  next: console.log,
  complete: () => console.log('done')
})

// unsubscribing later (optional)
setTimeout(() => {
   subscription.unsubscribe();
}, 60000);

that's really it in a nutshell.

(I'm NOT proposing this, just stating it for perspective...) If we created some JS-language level syntax for this, it could even look something like a generator function. function# or the like. It's just instead of returning a generator/iterator, you'd get an observable.