Skip to content

Observables should not 'restart' #216

@jakearchibald

Description

@jakearchibald

I know you're going to hate me for this, but I'd like to re-litigate the concerns I had on blink-dev:

const ob = new Observable((subscriber) => {
  subscriber.next(1);

  setTimeout(() => {
    subscriber.next(2);
  }, 1000);

  setTimeout(() => {
    subscriber.next(3);
    subscriber.complete();
  }, 2000);
});

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

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

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

The model above appears to be:

  • The SubscribeCallback is called synchronously the first time something subscribes to the observable.
  • Additional subscriptions do not result in SubscribeCallback being called again
  • Additional subscriptions may have missed values already emitted.

Even though this different to general observable behaviour, it makes sense to me. The fact that two synchronous subscriptions get different values is initially surprising, but it falls out of the synchronous nature of observables, and that's an essential part of the design.

However:

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

At this point, the behaviour seems to flip. The SubscribeCallback will be called again, since it's no longer active.

This seems internally inconsistent, and unreliable from the outside. In observable terms, it's both 'hot' and 'cold'. IMO one of the following patterns should be chosen:

  1. If values were emitted before you subscribed, tough luck, you missed them.
  2. The SubscribeCallback will be called again so you get all values from the start.

…rather than it being 'it depends'. Option 1 seems the best, meaning in the above toArray() example, the fulfilled value should be [].

The current behaviour makes other things weird:

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); // [] ⬅️⬅️
});

The first example exhibits the "get all the values from the start" behaviour. The second example tries to do this, but because the input is an iterator which doesn't support 'restarting', you get a seemingly inconsistent result.

When I raised this on blink-dev, folks said that my surprise at this was down to my inexperience with observables, and I accepted that. But, I've since talked to folks experienced in observables, and they seem surprised by this behaviour too.

Given that there's a single engine shipping this, and the above behaviour doesn't really come up when using element.when(…), I hope this can be changed without significant breakage.

@esprehn @bakkot @keithamus @acutmore what do you think? Am I way off here?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions