rxjs
rxjs copied to clipboard
`.next()` is available in observables returned by `.pipe()`
Describe the bug
The following code shows a weird behavior of .pipe():
const RxJs = require('rxjs');
const mySubj$ = new RxJs.Subject();
const doubled$ = mySubj$.pipe(
RxJs.map((v) => v * 2),
);
mySubj$.subscribe((value) => {
console.log('Original value:', value);
});
doubled$.subscribe((value) => {
console.log('Doubled value:', value);
});
mySubj$.next(1); // Logs:
// ✅ `Original value: 1`
// ✅ `Doubled value: 2`
doubled$.next(3); // Logs:
// ❌ `Original value: 3`
// ❌ `Doubled value: 6`
There's an inconsistency in the APIs exposed by doubled$:
.subscribe()applies the observable returned by the.pipe().next()applies to source of the.pipe()(mySubj$).
Expected behavior
I would expect .next() to fail with not a function.
According to the types, .pipe() returns an Observable (where .next() is not available).
Reproduction code
const RxJs = require('rxjs');
const mySubj$ = new RxJs.Subject();
const doubled$ = mySubj$.pipe(
RxJs.map((v) => v * 2),
);
mySubj$.subscribe((value) => {
console.log('Original value:', value);
});
doubled$.subscribe((value) => {
console.log('Doubled value:', value);
});
mySubj$.next(1); // Logs:
// ✅ `Original value: 1`
// ✅ `Doubled value: 2`
doubled$.next(3); // Logs:
// ❌ `Original value: 3`
// ❌ `Doubled value: 6`
Reproduction URL
No response
Version
7.8.2
Environment
No response
Additional context
Currently, this is the workaround to achieve this:
const doubled$ = mySubj$.asObservable().pipe(
Typescript types aren't exclusive. A property on a type means it exists, but the absence of a property does not mean that it doesn't.
(This is how e.g. private properties work, they still exist on the type and could be accessed at runtime, but they make the typechecker unhappy).
IMO, we shouldn't focus on the Typescript discussion. It was more of an additional point backing the bug report. Happy to remove it from the original description if it causes confusion.
IMO, the bug is better explained by the lack of consistency in the APIs returned in .pipe():
subscribe(),forEach(), andpipe()apply to the output of thepipe()(the result of calling any of those APIs differs when applied to the source or the output of the pipe,mySubj$vs.doubled$in the example).- However,
next()orcomplete()are always applied to the source, no matter if they are called from the output of thepipe().
For consistency, doubled$.next() (if exposed) should only emit to the subscribers of doubled$, and never to the subscribers of mySubj$. I'd argue that doubled$.next() shouldn't even go through the pipe(), since we're forcing a new emission of that observable (and not its source).
In code example, the following behavior would be consistent, IMO
doubled$.next(3);
// `Original value: 3` is not logged because we're emitting a new value from `doubled$`, not `mySubj$`.
// `Doubled value: 3` is logged because we're forcing `doubled$` to emit `3` (jumping the `.pipe()` since the API applies to the output of that `.pipe()`)
@afharo I hear where you are coming from. It's confusing behaviour.
On the other hand, the method you are calling is not part of the public API. The return type is Observable which does not expose a next() method. As soon as you call it, there is no defined "correct" behaviour.
This is JS land, you can abuse the API in any number of ways. Mutate BehaviorSubject._value directly and it won't emit to its subscribers. But that's not a bug, you're just not using it according to the spec.
@afharo I share in this sentiment. Here are my opinions;
- It's up to the library consumer to follow the guidelines of the spec and the library author to not encourage poor behaviors to whatever extent is best for their implementation. Personally, I am much more concerned about this as an author than I am as a consumer.
- JS can easily support this type of obfuscation at runtime via private propties (
#value) and abstraction - The public API types should match what is actually getting exposed at runtime. All it takes is for someone to
console.log(..,)or checkinstanceofto get access to an internal API that was never intended to be used publicly. If logic can be coupled to, it will be. I apply these principals in my own experimental libraries like; https://github.com/alexanderharding/buttercms/tree/main/projects/observable
JS can easily support this type of obfuscation at runtime via private propties (#value) and abstraction
Private properties are kinda bad performance wise if you have to downlevel them, which is what RxJS would do as it still publishes as es5/es2015.
@afharo do you have any interesting/good case for doubled$.next(), or it was just the inconsistency that caught your attention?
Are you not just trying to do something like the following, by any chance?
const doubled$ = new Subject().pipe(
mergeWith(mySubj$),
map((v) => v * 2),
);
@dariomannu, it was for the inconsistency. FWIW, I always treat the output of .pipe() as an observable, and don't force the .next() calls. But it raised the alarm that a bad actor might be able to inject external emissions to the original observable unless our code explicitly calls asObservable() before exposing it publicly.