go-react-redux-elm
go-react-redux-elm copied to clipboard
Fixing the Observable-powered state stream
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.
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;
}
}
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?
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.
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