Skip to content

.next() is available in observables returned by .pipe() #7543

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

Open
afharo opened this issue Apr 1, 2025 · 5 comments
Open

.next() is available in observables returned by .pipe() #7543

afharo opened this issue Apr 1, 2025 · 5 comments

Comments

@afharo
Copy link

afharo commented Apr 1, 2025

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(
@macksal
Copy link

macksal commented Apr 14, 2025

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).

@afharo
Copy link
Author

afharo commented Apr 14, 2025

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(), and pipe() apply to the output of the pipe() (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() or complete() are always applied to the source, no matter if they are called from the output of the pipe().

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()`)

@macksal
Copy link

macksal commented Apr 16, 2025

@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.

@alexanderharding
Copy link

alexanderharding commented Apr 16, 2025

@afharo I share in this sentiment. Here are my opinions;

  1. 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.
  2. JS can easily support this type of obfuscation at runtime via private propties (#value) and abstraction
  3. The public API types should match what is actually getting exposed at runtime. All it takes is for someone to console.log(..,) or check instanceof to 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

@JeanMeche
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants