go-react-redux-elm icon indicating copy to clipboard operation
go-react-redux-elm copied to clipboard

Fixing the Observable-powered state stream

Open appsforartists opened this issue 7 years ago • 4 comments

This is a continuation of https://github.com/tc39/proposal-observable/issues/120#issuecomment-317429350

Yeah, there are definitely weirdnesses in your current implementation. If you used a Subject rather than an Observable, you'd get observer pooling and caching for free, which would simplify a lot of what you have now. I think line 11 (let actionObserver = o) is a smell, and it's the part that confused you. Writing to the observer ought to be contained to the connect function whenever possible. By having a single value, cached outside, you've introduced the constraint that every new observer clobbers the old one.

If you use a Subject instead, you can write to the Subject's cache (and dispatch to all its observers) by calling subject.next(value), without having to store a reference to the observer.

appsforartists avatar Jul 24 '17 14:07 appsforartists

I did look at rxjs a little as I was going. Subjects looked like the right metaphor. However, subjects are not part of the TC39 Observable spec, correct? Does a spec exist somewhere else? What is the contract implied by the term Subject?

Would a subject implementation look something like this?

class Subject<T> {
  _observers: Set<Observer>;
  constructor() {
    this._observers = new Set();
  }
  error(error: Error) {
    this._observers.forEach(observer => {
      observer.error(error);
    });
  }
  next(event: T) {
    this._observers.forEach(observer => {
      observer.next(event);
    });
  }
  complete() {
    this._observers.forEach(observer => {
      observer.complete();
    });
  }
  subscribe(observerOrNext) {
    const observer = wrapWithObserver(observerOrNext);
    this._observers.add(observer);
    const subscription = {
      unsubscribe() {
        observer.disconnect();
        this._observers.delete(observer);
      },
    };
    observer.start(subscription);
    return subscription;
  }
  [Symbol.observable]() {
    return this;
  }
}

ajhyndman avatar Jul 25 '17 11:07 ajhyndman

Another point that's not clear to me (from reading the spec) is what should happen when you call .map() on an observable. Does connect get called? What about when I subscribe to the mapped observable? Does connect get called then?

ajhyndman avatar Jul 25 '17 12:07 ajhyndman

That looks about right. You can see my implementations here:

As you can see, I've separated the caching behavior from the observer pooling. Looks like you don't have caching at all in yours, though most implementations do. I don't use error or complete, so I haven't researched their semantics in other implementations.

Does a spec exist somewhere else? What is the contract implied by the term Subject?

I don't believe there is a spec for Subject. A Subject is both an Observable and an Observer - that is, it has both subscribe and next methods. Calling next passes the value to all the observers that subscribe has received.

what should happen when you call .map() on an observable. Does connect get called? What about when I subscribe to the mapped observable? Does connect get called then?

I've written an explainer on how Observables work. Here's the link:

https://material-motion.github.io/material-motion/documentation/IndefiniteObservable/

Let me know if that helps clear things up for you. André (@staltz) is also a good source on the topic.

appsforartists avatar Jul 25 '17 23:07 appsforartists

Thanks for sharing some of your work. I'm going to do a bit of thinking out loud, if you'll indulge me:

There's still something about the TC39 Observable API that seems to grate on me. I think it might be that a lot of the terminology borrows from list operations (map, reduce, next) — and indeed an event stream and a list are similar concepts — yet this seems to have set me up with a lot of false expectations as a user. Lists are a good analogy, but it's not obvious where the analogy breaks down.

The Observable implementation is quite thin, which is nice when you grok it. But it also means that a lot of an Observable's behaviour is dependent on the connect implementation, which is left to the application code author.

Perhaps I'm just expecting too much. Maybe an Observable is a lower-level concept, and higher level compositions (like Subject) are what I'm after.

I guess... it's not quite clear to me what I get out of adhering to the Observable spec, yet. I definitely would like to see a healthy ecosystem of abstractions and transformers built around event streams, but I have found it surprisingly difficult to reason about my use-cases with existing terms and primitives.

To be concrete again: The Redux pattern seems like one of the most common use cases in the present ecosystem, and it's weird to me that the simplest implementations don't rely on Observable at all (for Subjects, you and I both conformed to the interface, but didn't bother extending or composing an Observable).


This is the behaviour that I think I expect, when I naively apply the list metaphor to an observable event stream:

https://github.com/ajhyndman/go-react-redux-elm/blob/redux-db/server/index.js#L5-L24

ajhyndman avatar Jul 26 '17 11:07 ajhyndman