-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Discuss] expose BahviorSubject instead of Observable #65224
Comments
Pinging @elastic/kibana-platform (Team:Platform) |
probably will be done in #53268 as part of |
@restrry that would be great, however, my understanding is that "config" in that sentence refers to |
I'm 👍 on implementing this for most of Core's observables where the observable emits the "latest value".
That config is referring to |
The downside to this is that it's a backdoor to go from reactive code to imperative. Sometimes it's necessary or very awkward not to do it, I think particularly when you need to read a config value from kibana.yml to call a Core API from If you have a long-lived class your example would be better left as an subscribe to make sure currentAppId stays up to date: core.application.currentAppId$
.subscribe((appId: string | undefined) => (this.currentAppId = appId)); But something like const shortLivedVariable = await core.application.currentAppId$
.pipe(take(1)).toPromise(); Would benefit from using a |
You forgot to unsubscribe in that example. 🤓 I would argue in a long-lived class, if you are not using any or RxJS programming patterns, you are still better off using this.core.application.currentAppId$.getValue() or wrapping it into a local helper private readonly currentAppId = () => this.core.application.currentAppId$.getValue(); reason being you don't need to unsubscribe and you don't need to know RxJS (and less code). |
Another issue with having to |
I've seen need for this as well, particularly when setting the initial state in a react component. Imperative code works for this in ways that reactive code does not mostly due to typescript. |
Overall I understand the need and I'm +1 on that, even if imho we should be cautious before implementing it everywhere. There may be places where we want to avoid synchronous obs value getters, simply to encourage the reactive approach instead (or/and because using non-reactive approach can be dangerous).
Taking a random example from kibana/src/core/public/application/application_service.tsx Lines 277 to 281 in 4330865
Also not all our observable are behaviorSubjects, meaning that some can have NO last emitted value. So the actual type would be (ihmo we should not name if from // yea the name suck. Please find something better
type ObservableWithGetValue<T> = Observable<T> & {
getLastValue(): T | undefined;
}; We would need to introduce a wrapper, something like // very naive implem. Please break it. Maybe we will need our own `Observable` subclass instead.
// i.e what should be the `lastValue` once the source obs is completed?
function addGetLastValue<T>(obs: Observable<T>): ObservableWithGetValue<T> {
let lastValue: T | undefined;
const piped = obs.pipe(
tap(value => {
lastValue = value;
})
);
return Object.assign(piped, {
getLastValue: () => lastValue,
});
} and use it like currentAppId$: addGetLastValue(this.currentAppId$.pipe(
filter(appId => appId !== undefined),
distinctUntilChanged(),
takeUntil(this.stop$)
)), |
That I would suggest a simple function that converts const observableToBehaviorSubject = <T>(currentValue: T, obs: Observable<T>): BehaviorSubject<T> => {
const subj = new BehaviorSubject(currentValue);
obs.subscribe(subj);
return subj;
}; and then currentAppId$: observableToBehaviorSubject(
this.currentAppId$.getValue(),
this.currentAppId$.pipe(
filter(appId => appId !== undefined),
distinctUntilChanged(),
takeUntil(this.stop$)
)
), |
Yea it does. But as I already said, all our exposed observable are not necessarily chained from Subjects.
Would need other team member's opinion, but I personally really don't like the idea of returning concrete subjects from our APIs. Also this doesn't work if you don't know the |
This proposal is only for subjects. I'm not suggesting to do anything for plain observables.
It does not need to be type ReadonlyBehaviorSubject<T> = Observable<T> & Pick<BehaviorSubject<T>, 'getValue'>; Or it could have Benefit of using currentAppId$: ReadonlyBehaviorSubject<string | undefined> = ... |
As we are only returning Observable in our APIs, this suggestion means that the feature would/should only be available on some of the APIs depending on the implementation detail that we are, or not, using a Subject as the observable's source? What about a plain Observable with a To take a concrete example from the kibana/src/core/public/chrome/chrome_service.tsx Lines 111 to 130 in 4330865
Adding a sync accessor for the Even If we accept the postulate that
With your given |
It would be great if you would add sync accessors for those, too. For that few extra lines would need to be added on top of
I'm not familiar with code there, but my initial guess would be that maybe this could work: getIsVisible$: ReadonlyBehaviorSubject = () =>
observableToBehaviorSubject(false, this.isVisible$);
I'm not sure I would call it "hiding", it is just casting. Curious, what is your point here? If you do Let's say you have: class Animal {}
class Dog extends Animal {}
const dog = new Dog(); and method function feedAnimal(anima: Animal) {} would you do? feedAnimal(dog); or? feedAnimal(dog.toAnimal()); |
We introduced Observable-based APIs as a forcing function on consumers to be handle changing values. I believe introducing a sync access pattern will inevitably lead to bugs where consumers are not handling changing values and utilize stale data. So if we were to introduce a sync pattern, we should do so carefully and only with APIs where having a stale value would not be potentially dangerous. Even then, I'm not sure the tradeoff here is worth (increased convenience vs. introducing possible bugs). In terms of how we would do this, I'd prefer that we implement our own interface for this that does not risk exposing the BehaviorSubject API at all, and is essentially a extension of // this is invalid b/c Observable is a class, but you get the idea
interface LastValueObservable extends Observable<T> implements SubscriptionLike {
// value may not be defined yet, we could either return undefined or
// throw an error if this is called before a value has been emitted
// (which would indicate this is being used with an API that does not have a
// replay effect)
getLastValue(): T | undefined;
} One thing to consider as well is that we generally expose Observables via a function that returns a new Observable. With this pattern, we would need to implement What we need to figure out though is whether or not the tradeoffs above are worth the risks. @streamich could you point to specific examples in our codebase where introducing this would significantly improve the implementation? |
I'll assume we don't need this anymore and close the issue. Please feel free to reopen otherwise. |
TLDR; Add
.getValue()
method to observables exposed from core services which have concept of current value.Problem
Often platform APIs expose observables to plugins, which is great for changing data. However, sometimes those observables have current value which they emit immediately on subscription. Often devs are interested just in the current value, however there is no convenient way to fetch it, devs need to subscribe to the observable, take one value and unsubscribe.
To get the current app ID, right now devs need to do somethings like this:
Instead, it could be something like this:
In this case it would even be possible to put it at class level:
Solution
Expose observables that have concept of current value as "behavior subject". It does not need to be a complete
BehaviorSubject
, it just needs to have.getValue()
method. It could be calledReadonlyBehaviorSubject
.Where?
Above was an example of Application service, but this applies to a number of other places where observable with current value is exposed, here are some such observables in Chrome service:
The text was updated successfully, but these errors were encountered: