-
Notifications
You must be signed in to change notification settings - Fork 27k
Prototype of the RxJS interop layer for signals #49154
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
Conversation
jelbourn
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM barring open comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit unusual to use the $ suffix on a promise. Besides, toPromise() is deprecated in RxJS7.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, good catch - fixed.
Angular's on RxJS 6 though, so no fancy latestValueFrom for us.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What stoping you from moving to a higher version of Rxjs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Google uses RxJS 6 internally, so Angular needs to build with it.
We could (and probably will) upgrade to using 7, but we'd still need to be compatible with 6, so it's not been a priority.
4730e8e to
d94877e
Compare
|
can I remove zone when I use Signal? |
josephperrott
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM for dev-infra
Note: this will require us to create this package on npm prior to being able to perform a release after this pull request lands.
@alxhub I also wanted to ask, why is this not in core as well? It seems to me that it makes more sense to have this colocated with signals rather than ths which feels a little like an indirection.
|
Hey @alxhub! Nice work! But why Signal type dont just implement the InteropObservable type to build the bridge between rxjs and signals? Is there a reason? Think how the usage sounds better: combineLatest([myPrimitiveSignal, myObservable$]) // ...and so on! |
packages/rxjs-interop/PACKAGE.md
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to mention signals explicitly here? I guess long-term we want to put other non-signal, RxJS related stuff here.
packages/rxjs-interop/README.md
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to mention signals / reactivity explicitly here? I guess long-term we want to put other non-signal, RxJS related stuff here.
packages/rxjs-interop/package.json
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need explicit peer dep on zone.js?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alxhub Can you explain please this part please?
I mean it was a rxjs-interop package, probably the plan was @angular/rxjs-interop.
Now as I see, rxjs-interop will be the part of core, so zone.js is still will be a dependency there. As I heard, signals are advertised as zoneless solutions, which is currently the only (main) reason for me why we should use them.
I would like to understand that this pull request is just a step forward or there are other reason why we still need zone.js.
I think having another npm package as @angular/rxjs-interop or something is better until zone.js cannot be eliminated from core. In long term I agree, it should be in core library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find it surprising that the type of the signal and initial state are different. What is the use-case that we are targeting here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My guess is that it's mainly useful when you have an Observable<Something> and you want null or undefined as default value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
implementation of rxjs firstValueFrom and lastValueFrom also have different type for defaultValue.
But maybe instead of initialValue rxjs defaultValue is more fitting as it is not only initial - so in a state of suspense but also if observable completed without any value too. Also keeping rxjs config approach can be useful in future if we would like to extend this config,
export interface FirstValueFromConfig<T> {
defaultValue: T;
}
export function firstValueFrom<T, D>(source: Observable<T>, config: FirstValueFromConfig<D>): Promise<T | D>;
export function firstValueFrom<T>(source: Observable<T>): Promise<T>;
Then again If the source Observable does not complete or emit, you will end up with a Promise that is hung up, and potentially all of the state of an async function hanging out in memory,
So they differentiate those two states and do not throw nor return a default value if there is no emission before read.
Initial value could also be achieved by using startWith operator but that would not work if the subsequent emission would be empty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's mainly useful when you have an Observable and you want null or undefined as default value.
But this is strange since having null as a default value means that anyone can read this value so it should be legitimate type of a signal. In this case it would be T | null
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Defaulting to null | undefined instead of initialValue: U would be | async pipe | null problem again
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just throwing in my 2 cents here...
RxJS doesn't distinguish between observables that emit immediately vs asynchronously. So I don't think consumers of observables should rely on knowledge about when they're going to emit. This could cause unexpected errors. Much better would be to type it as T | null always, but optimally let devs pass in a type argument (or infer from initial state passed in) if they happen to know 100% that the observable emits immediately. But in my opinion even this explicit type parameter is dangerous because you could modify the observable upstream and TypeScript could not prevent a runtime error. And I don't see a significant difference between null and undefined here, but I haven't thought that through carefully.
The only way safe way out of this TypeScript awkwardness is if the observables themselves contained knowledge about whether they are going to emit immediately or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking about this before, not sure if its possible for the given use case, but you could introduce a 'loading' object which is static and use it as a type. That way you could have a difference between loading, null, undefined and T.
What you would have to do if handle then that type in every pipe in angular etc. Maybe a overkill but just wanted to point the idea out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it computed? I guess there are 2 things we need to do:
- make sure that it is not mutable;
- it is branded (?)
Normally a simple function would be enough, I guess this is about branding?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What dose it mean 'branded'?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
7ae3be6 to
377013b
Compare
baf1ef3 to
b833473
Compare
b833473 to
78fa475
Compare
We've been experimenting with the DeepReadonly type that would make signal values deeply read-only and prevent accidental changes without going to the owner of data. What we've found out during the experiments is that additional safety net has more drawbacks than benefits: it just introduces too much friction to be practical for daily usage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: should it be RuntimeError?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: TODO is left in. This needs a runtime error code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thinking out loud: those tests will fail when we change the timing of effects, right? Which kind of rises the question of testing patterns for effects. A separate discussion that we should have.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As an alternative we could have written those tests with just a signal and / or computed and just assert that new value is available. We might leave a test or two with effect but IMO ones based on just signals would be more readable.
pkozlowski-opensource
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
Left a set of bunch of nit / "thinking out loud" comments but nothing that would block this PR. We will need to coordinate this PR landing with other in-flight ones as we will have conflicts around DeepReadonly and timing of effects.
Reviewed-for: public-api
Reviewed-for: fw-core
This commit adds the infrastructure for `@angular/core/rxjs-interop`, a new core entrypoint which provides bidirectional interoperability between Angular's built-in reactivity system of synchronous signals, and the RxJS `Observable` abstraction. The new entrypoint is set up as an empty shell in this commit, with its implementation to follow in a followup.
This commit adds the basic sketch for the implementation of `fromObservable` and `fromSignal`, the two basic primitives which form the RxJS interop layer with signals.
This commit implements an RxJS operator `takeUntilDestroyed` which terminates an Observable when the current context (component, directive, etc) is destroyed. `takeUntilDestroyed` will inject the current `DestroyRef` if none is provided, or use one provided as an argument.
78fa475 to
877d100
Compare
jessicajaniuk
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM other than a non-blocking nit
reviewed-for: fw-core, public-api
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: TODO is left in. This needs a runtime error code.
jessicajaniuk
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM other than a non-blocking nit
reviewed-for: fw-core, public-api
|
This PR was merged into the repository by commit e883198. |
This commit adds the infrastructure for `@angular/core/rxjs-interop`, a new core entrypoint which provides bidirectional interoperability between Angular's built-in reactivity system of synchronous signals, and the RxJS `Observable` abstraction. The new entrypoint is set up as an empty shell in this commit, with its implementation to follow in a followup. PR Close #49154
This commit implements an RxJS operator `takeUntilDestroyed` which terminates an Observable when the current context (component, directive, etc) is destroyed. `takeUntilDestroyed` will inject the current `DestroyRef` if none is provided, or use one provided as an argument. PR Close #49154
If you already decided on it, why are you discussing it in RFC? |
|
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
For context, see this Github Discussion on our experiments with fine-grained reactivity in Angular
(the first commit here is #49150 which serves as a base for this implementation)
This is the prototype implementation of the RxJS interop layer, sketched out as a separate package (
@angular/rxjs-interop).