Groups keyboard shortcuts have been updated
Dismiss
See shortcuts

Intent to Ship: Observable API

6,140 views
Skip to first unread message

Chromestatus

unread,
Feb 21, 2025, 4:25:05 PMFeb 21
to blin...@chromium.org, b...@benlesh.com, d...@chromium.org

Contact emails

d...@chromium.org

Explainer

https://github.com/WICG/observable

Specification

https://wicg.github.io/observable

Summary

Observables are a popular reactive-programming paradigm to handle an asynchronous stream of push-based events. They can be thought of as Promises but for multiple events, and aim to do what Promises did for callbacks/nesting. That is, they allow ergonomic event handling by providing an Observable object that represents the asynchronous flow of events. You can "subscribe" to this object to receive events as they come in, and call any of its operators/combinators to declaratively describe the flow of transformations through which events go. This is in contrast with the imperative version, which often requires complicated nesting with things like `addEventListener()`. For more on this, see the examples in the explainer. The big selling point for native Observables is their integration with EventTarget — its proposed `when()` method that returns an Observable which is a "better" `addEventListener()`. See https://github.com/WICG/observable and https://twitter.com/domfarolino/status/1684921351004430336. See the spec https://wicg.github.io/observable/ and the design doc: https://docs.google.com/document/d/1NEobxgiQO-fTSocxJBqcOOOVZRmXcTFg9Iqrhebb7bg/edit.



Blink component

Blink>DOM

TAG review

https://github.com/w3ctag/design-reviews/issues/902

TAG review status

Issues addressed

Risks



Interoperability and Compatibility

Initially we proposed adding the `.on()` method to EventTarget, which was found to conflict with userland versions of the same method. The conflict was found to be too significant to justify shipping our native version of this API (see https://github.com/WICG/observable/issues/39) so we renamed it to `.when()` and we strongly believe this resolves any naming collision issues after searching through public libraries and performing developer outreach on X. See the discussion on that issue.



Gecko: No signal (https://github.com/mozilla/standards-positions/issues/945)

WebKit: Positive (https://github.com/WebKit/standards-positions/issues/292)

Web developers: Strongly positive (https://twitter.com/domfarolino/status/1684921351004430336) Also see https://foolip.github.io/spec-reactions/ and the developer interest in the original WHATWG DOM issue.

Other signals: We've gotten good design feedback from TC39 members on many issues which we have implemented accordingly. This has led to positive feedback from Node.js, and luke-warm non-negative feedback from WinterCG. See https://github.com/WICG/observable/issues/93; specifically https://github.com/nodejs/standards-positions/issues/1 & https://github.com/WICG/observable/issues/30 for Node, and https://github.com/wintercg/proposal-minimum-common-api/issues/72 for WinterCG.

WebView application risks

Does this intent deprecate or change behavior of existing APIs, such that it has potentially high risk for Android WebView-based applications?

None



Debuggability

The developer experience of Observables might benefit from Observable-specific DevTools tracking of events and streams (see https://github.com/WICG/observable/issues/55). It is possible that the existing DevTools work that assists asynchronous task tracking and callstack tagging may be sufficient though. At the moment, however, our effort is focused on the platform implementation of Observables.



Will this feature be supported on all six Blink platforms (Windows, Mac, Linux, ChromeOS, Android, and Android WebView)?

Yes

Is this feature fully tested by web-platform-tests?

Yes

See https://wpt.fyi/results/dom/observable/tentative.



Flag name on about://flags

observable-api

Finch feature name

ObservableAPI

Requires code in //chrome?

False

Tracking bug

https://bugs.chromium.org/p/chromium/issues/detail?id=1485981

Estimated milestones

Shipping on desktop 135


Anticipated spec changes

Open questions about a feature may be a source of future web compat or interop issues. Please list open issues (e.g. links to known github issues in the project for the feature specification) whose resolution may introduce web compat/interop risk (e.g., changing to naming or structure of the API in a non-backward-compatible way).

Issues with the "possible future enhancement" label [1] track possible changes to the feature that may come after we ship the initial API. One issue (https://github.com/WICG/observable/issues/200) is identified to have behavior changes that theoretically pose a compat risk, but only for developers that subclass the API. The behavior change proposed puts the implementation more inline with what subclass users want: the operators that return native Observable objects would instead return objects of `this.constructor` type, as to return instances of the subclass that the operators are called on. This is how JS built-ins like `Array` work, however, no other web platform feature works like this and it likely requires non-trivial Web IDL support. [1]: https://github.com/WICG/observable/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22possible%20future%20enhancement%22

Link to entry on the Chrome Platform Status

https://chromestatus.com/feature/5154593776599040?gate=5141110901178368

Links to previous Intent discussions

Intent to Prototype: https://groups.google.com/a/chromium.org/d/msgid/blink-dev/CAP-uykBH1%3DUoLN6%3DBRSEZE%2B1iUq6UdcTpo3qtTQ5T%3DSRxwnu5Q%40mail.gmail.com


This intent message was generated by Chrome Platform Status.

Domenic Denicola

unread,
Feb 25, 2025, 12:17:01 AMFeb 25
to Chromestatus, blin...@chromium.org, b...@benlesh.com, d...@chromium.org
Very exciting!

https://github.com/WICG/observable/issues/177 looks like it might also have compat impacts, right? Albeit only in error cases. (In fact, in cases where there are two errors at once.)

Do you think it might be worth making that change before shipping?
 
--
You received this message because you are subscribed to the Google Groups "blink-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to blink-dev+...@chromium.org.
To view this discussion visit https://groups.google.com/a/chromium.org/d/msgid/blink-dev/67b8ef25.2b0a0220.38f609.0192.GAE%40google.com.

Yoav Weiss (@Shopify)

unread,
Feb 25, 2025, 2:34:16 AMFeb 25
to blink-dev, Domenic Denicola, blin...@chromium.org, b...@benlesh.com, Dominic Farolino, Chromestatus
This is indeed exciting!!

How do you imagine developers adopting this before support is ubiquitous? Is there a "standard" polyfill developers are expected to use? Some feature-detection based patterns? Something else?

To unsubscribe from this group and stop receiving emails from it, send an email to blink-dev+unsubscribe@chromium.org.

Jake Archibald

unread,
Feb 26, 2025, 2:55:06 PMFeb 26
to blink-dev, Chromestatus, b...@benlesh.com, d...@chromium.org
I'm struggling a little to get my head around how this works.

https://codepen.io/jaffathecake/pen/raNWMmK?editors=0012 - it seems inconsistent that two of the calls to ob.map create a new subscriber, whereas the other picks up the observable half way through.

If I put the .complete call in a setTimeout, then there's only one subscriber created.

I guess the rule is: A new subscriber is created if the observer has closed, but isn't this really inconsistent?

I'd expect it to be one way or the other. As in:

There's only one call to the subscriber.
Or
Each call to the initial observable is a new subscription.

The way it's neither one or the other is confusing to me. But maybe that's totally normal for folks who are used to observables?

Dominic Farolino

unread,
Feb 26, 2025, 3:39:53 PMFeb 26
to Jake Archibald, blink-dev, Chromestatus, b...@benlesh.com
https://codepen.io/jaffathecake/pen/raNWMmK?editors=0012 - it seems inconsistent that two of the calls to ob.map create a new subscriber, whereas the other picks up the observable half way through.

Right, the idea that a subscription doesn't have side effects if an existing subscription is in-flight was essentially the outcome of https://github.com/WICG/observable/issues/170 & https://github.com/WICG/observable/issues/178. The alternative, where producer:consumer are 1:1, made it easy to write performance foot-guns where what you actually want is to tap into an existing stream of values without paying the cost of setting it up each time if it already exists. Many userland Observables inevitably get `share()` slapped on them somewhere in the chain to alleviate this, but the inconsistency made it hard to judge whether your subscription would have side-effects or not. We also saw a lot of Observable learning material was taking pains to caveat right away, this unintuitive idea that the Observable type itself doesn't represent anything but a stateless subscription vendor. Now it basically represents the producer, and I think that matches peoples' mental models.

I guess the rule is: A new subscriber is created if the observer has closed, but isn't this really inconsistent?

I think it is consistent though, no? It's true that it's neither "only one call to the subscriber" nor "each call to the initial observable initiates a new subscription". But it is similar to what you wrote above: a subscriber is invoked/spun up if its subscription is closed (not observer). The pay-off is that you know you're never going to have "extra" side effects when subscribing. At most you will spin up a single producer (which you're OK with since you're subscribing), and at best you will listen in on an existing one. If you need a new subscriber to be created on each subscription, you'll need to basically take a closure over the Observable-vending API and call each `subscribe()` on it, which I hope is not too burdensome.

Dominic Farolino

unread,
Feb 26, 2025, 3:50:12 PMFeb 26
to Jake Archibald, blink-dev, Chromestatus, b...@benlesh.com
Responding to Yoav below:

How do you imagine developers adopting this before support is ubiquitous? Is there a "standard" polyfill developers are expected to use? Some feature-detection based patterns? Something else?

Great question. RxJS, the main userland Observable library, is working to provide a polyfill (also see this post) for folks to use. And the next major version of RxJS will also be backwards compatible with the native implementation, per https://x.com/BenLesh/status/1893053275995357608 and https://github.com/ReactiveX/rxjs/issues/6367.

Jake Archibald

unread,
Feb 26, 2025, 3:53:47 PMFeb 26
to Dominic Farolino, blink-dev, Chromestatus, b...@benlesh.com
On Wed, 26 Feb 2025 at 20:39, Dominic Farolino <d...@chromium.org> wrote:
https://codepen.io/jaffathecake/pen/raNWMmK?editors=0012 - it seems inconsistent that two of the calls to ob.map create a new subscriber, whereas the other picks up the observable half way through.

Right, the idea that a subscription doesn't have side effects if an existing subscription is in-flight was essentially the outcome of https://github.com/WICG/observable/issues/170 & https://github.com/WICG/observable/issues/178. The alternative, where producer:consumer are 1:1, made it easy to write performance foot-guns where what you actually want is to tap into an existing stream of values without paying the cost of setting it up each time if it already exists. Many userland Observables inevitably get `share()` slapped on them somewhere in the chain to alleviate this, but the inconsistency made it hard to judge whether your subscription would have side-effects or not. We also saw a lot of Observable learning material was taking pains to caveat right away, this unintuitive idea that the Observable type itself doesn't represent anything but a stateless subscription vendor. Now it basically represents the producer, and I think that matches peoples' mental models.

I guess the rule is: A new subscriber is created if the observer has closed, but isn't this really inconsistent?

I think it is consistent though, no? It's true that it's neither "only one call to the subscriber" nor "each call to the initial observable initiates a new subscription". But it is similar to what you wrote above: a subscriber is invoked/spun up if its subscription is closed (not observer). The pay-off is that you know you're never going to have "extra" side effects when subscribing. At most you will spin up a single producer (which you're OK with since you're subscribing), and at best you will listen in on an existing one.

I think where it gets confusing is when the observable has a beginning and an end. It's fine for event targets, because they don't have that.

For event target observables it's 'interested' (add the listener) and 'distinerested' (remove the listener). Whereas the underlying events are still continuing.

With the example in the codepen, as the holder of the observable, I don't think I have a way of knowing if I'm getting the start, or something in the middle. Isn't that a bit odd? If it's ok to miss the start, why isn't it ok to miss the end?

Again it might be because I'm not used to the patterns, and they're well understood elsewhere.

Domenic Denicola

unread,
Feb 27, 2025, 1:00:56 AMFeb 27
to Jake Archibald, Dominic Farolino, blink-dev, Chromestatus, b...@benlesh.com
I looked into the SuppressedError proposal a bit more. I'm now about 90% convinced SuppressedError does not need to be used. (Or if there is a case for it, it's in extreme edge cases that we could address after shipping.)

Given how complete every other aspect of this Intent is, LGTM1, conditional on Dominic agreeing with my reasoning that we don't want to use SuppressedError for most callbacks. If I misunderstood, then we should delay until that gets straightened out.

--
You received this message because you are subscribed to the Google Groups "blink-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to blink-dev+...@chromium.org.
To view this discussion visit https://groups.google.com/a/chromium.org/d/msgid/blink-dev/CAJ5xic-3d9ziBOmqHYiSGPxLmDzhu19vfbQHffqJSkprFcE%2Btg%40mail.gmail.com.

Dominic Farolino

unread,
Feb 28, 2025, 10:20:58 AMFeb 28
to Domenic Denicola, Jake Archibald, blink-dev, Chromestatus, b...@benlesh.com
I was told that it would be good to clarify something here from the original email sent here, about TC39's engagement and WebKit's standards position.

WebKit: Positive (https://github.com/WebKit/standards-positions/issues/292)
 
I marked WebKit's standards position as positive since Anne had mentioned WebKit folks were supportive and he recommended marking the issue as `position: support`. However, he has since walked it back since the proposal has not been formally presented to TC39. Such a presentation is abnormal for web APIs not developing within TC39, however is still a reasonable idea that I am happy to do. (For what it's worth, I tried to present this proposal to TC39 at the Tokyo virtual meeting in October 2024 after TPAC last year, and unfortunately after staying up late to do, so I got bumped from the agenda last minute because other items went over time).

Regarding my comment on TC39 engagement, I wrote:

We've gotten good design feedback from TC39 members on many issues which we have implemented accordingly.

This is true—various ECMAScript editors have engaged with us on substantial design issues. However, since we have not formally presented to TC39, I was made aware that this kind of engagement might not count as proper TC39 engagement. So I wanted to call out here that we have not yet sought or received any kind of formal "sign-off" by ECMAScript editors on our proposal.

Dominic Farolino

unread,
Feb 28, 2025, 10:32:54 AMFeb 28
to Domenic Denicola, Jake Archibald, blink-dev, Chromestatus, b...@benlesh.com
Responding to Jake:

With the example in the codepen, as the holder of the observable, I don't think I have a way of knowing if I'm getting the start, or something in the middle. Isn't that a bit odd?

Hmm, I see how it can feel a little intuitive, but I think this tradeoff is less unintuitive than it was without ref-counted producers, where Observable doesn't really represent anything related to the subscription, causing the footguns that led to us pursuing this path in the first place. Regarding:

If it's ok to miss the start, why isn't it ok to miss the end?

I don't think it is OK to miss the end, and I don't quite think our proposal makes this possible? If you subscribe half-way through, you will still get `complete()` notifications so you know that the stream has ended. The closest example of "missing the end" I can think of would be the one you mentioned over X/Twitter, which is if you subscribe to an async iterator (not iterable that can be restarted), and you exhaust the iterator, what do subsequent subscriptions do after the iterator is exhausted?

  async function* asyncNumbers() {
    yield* [1,2,3,4];
  }

  const ob = Observable.from(asyncNumbers());

  await ob.toArray().then(result => console.log('one', result));
  ob.subscribe({
      next: v => console.log('second subscription: ', v),
      complete: () => console.log('complete'),
  })

By the time the second subscription rolls around, the iterator has been exhausted. But you still don't "miss the end" since the `complete()` handler fires. Hopefully that makes sense. Either way, it's entirely possible this thread isn't the best place to hash all of this out :)

Jake Archibald

unread,
Feb 28, 2025, 12:32:28 PMFeb 28
to Dominic Farolino, Domenic Denicola, blink-dev, Chromestatus, b...@benlesh.com
By missing the end, I mean this:

const ob = new Observable((subscriber) => {
  subscriber.next(1);
  setTimeout(() => {
    subscriber.next(2);
    subscriber.complete();
  }, 1000);
});

ob.toArray().then((vals) => {
  // You're first, so you get things from the start.
  console.log(vals); // [1, 2]
});

ob.toArray().then((vals) => {
  // You missed the start, so you get the remaining values.
  // I'd describe the model here as: too late, you miss out!
  console.log(vals); // [2]
});

setTimeout(() => {
  ob.toArray().then((vals) => {
    // You missed the end, so we restart.
    // I'd describe the model here as: we'll fix it so you don't miss out
    console.log(vals); // [1, 2]
  });
}, 1500);


The bit where it sometimes restarts the thing so you don't miss out, and sometimes doesn't, felt unusual to me. To be clear, I think the ref-counting approach is right, but it would have felt more consistent if the final log was [], since the thing had already completed.

The other case I found inconsistent is:

const ob = Observable.from([1, 2, 3]);

ob.toArray().then((vals) => {
  console.log(vals); // [1, 2, 3]
});

ob.toArray().then((vals) => {
  console.log(vals); // [1, 2, 3]
});

vs

const ob = Observable.from([1, 2, 3].values());

ob.toArray().then((vals) => {
  console.log(vals); // [1, 2, 3]
});

ob.toArray().then((vals) => {
  console.log(vals); // []
});


I had a meeting with Dominic and I now understand why it happens. It still seems unusual, but given that this hasn't come up for anyone else looking at the API (people who have way more experience with observables than I do), I guess it's just that I'm unfamiliar with these patterns. It's certainly something I'd call out in developer documentation for others coming to this fresh.

Thanks all!
Jake.

Dominic Farolino

unread,
Mar 5, 2025, 3:57:55 PMMar 5
to Jake Archibald, Domenic Denicola, blink-dev, Chromestatus, b...@benlesh.com
Thanks for the summary Jake. So from the perspective of API OWNERS, I don't believe anything is blocking here. Folks agree that the ref-counted producer design is the right way, consistent with the developer feedback, and while you can hold the API in a way that appears surprising, (1) there are more surprises/quirks with the non-ref-counted approach, and (2) our design is consistent with the ways developers use Observables in the wild, mitigating the consequences of any surprises—I believe we're making the right trade-off.

Given that, I believe we can proceed with the review.

Chris Harrelson

unread,
Mar 5, 2025, 4:00:37 PMMar 5
to Dominic Farolino, Jake Archibald, Domenic Denicola, blink-dev, Chromestatus, b...@benlesh.com

Chris Harrelson

unread,
Mar 5, 2025, 4:03:21 PMMar 5
to Dominic Farolino, Jake Archibald, Domenic Denicola, blink-dev, Chromestatus, b...@benlesh.com
Sorry, make that LGTM2

Mike Taylor

unread,
Mar 5, 2025, 5:02:47 PMMar 5
to Chris Harrelson, Dominic Farolino, Jake Archibald, Domenic Denicola, blink-dev, Chromestatus, b...@benlesh.com
Reply all
Reply to author
Forward
0 new messages