owned this note
owned this note
Published
Linked with GitHub
# Design Doc - Push Pipe
author: [Michael Hladky]
created: 2020-02-01
status: [draft | in review | approved | **implemented** | obsolete]
doc url: https://hackmd.io/Uus0vFu3RmWRVGgmtzuUWQ
feedback url: https://github.com/rx-angular/rx-angular
design doc version: 1.0.0
## Objective
<!--
A rough overall explanation of the idea as well as related terms and a quick scatch of the need.
-->
Subject of this design doc is to create a pipe, similar to `async`pipe.
It takes [asynchronous primitives](https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10#asynchronousobservable-primitive) and renders thier value to the template.
The main goal is to have such a pipe also work zone-less as **drop-in replacement**.
As this document uses specific terminology find detailed explanations in the gist here: [Reactive Angular Terminology](https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10).
The pipe solves following [primitive recative problems](https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10#primitive-recative-problems):
- Retreive asynchronous primitives
- Subscription handling
- Change detection and rendering
Especially rendering should be done only over the `ngrxPush` or `*ngrxLet` as the render process is different from template expressions to template bindings.
Handling of change detection in the component would make it impossible to interact with either only the EnbeddedView or the ComponentView and would lead to unwanted performance drawbacks. This topic is explained in-depth in another document listed in the caveats section.
### Expected User/Developer Experience
<!--
Describe what will the user be able to do and how will they be able to do it thanks to this new feature. This should be done in a story format, described from the user’s/developer’s perspective, with as many details as possible but without going into the implementation details.
-->
With the `ngrxPush` pipe the user will be able to create reactive applications that can switch between zone-full and zone-less mode without changing the codebase.
This is done by using the pipe in the template in the same way he would use the `async` to [bind](https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10#bind) observables to it.
The user can use any observable that can be referenced over the component class.
File: **any.component.ts**
```typescript
import { Component, Input } from '@angular/core';
import { Subject, interval } from 'rxjs';
@Component({
selector: 'any',
templateUrl: 'any.component.html'
// Implementation works independent of the ChangeDetectionStrategy
})
export class AnyComponent {
inputDecoratorSubject$ = new ReplaySubject<any>(1);
@Input()
set value(value: any) {
this.inputDecoratorSubject$.next(value);
};
classInternalObservable$ = interval(1000);
routerParams$ = this.activatedRoute.params;
constructor(public activatedRoute: ActivatedRoute,
public store$: Store<any>) {
}
}
```
and bind it's valued to the part of the template the pipe is used.
File: **any.component.html**
```htmlmixed
{{inputDecoratorSubject$ | ngrxPush}}
<ng-container *ngIf="classInternalObservable$ | ngrxPush as o">{{o}}</ng-container>
<component [value]="routerParams$ | ngrxPush"></component>
{{store$ | ngrxPush}}
```
If there is already existing code that uses the `async` pipe the `ngrxPush` pipe can be used as a drop-in replacement.
```htmlmixed
<!-- Before: -->
{{observable$ | async}}
<!-- After: -->
{{observable$ | ngrxPush}}
```
### Background
<!--
Stuff one needs to know to understand this doc: motivating examples, previous versions, and problems, links to related projects/design docs, etc. You should mention related work if applicable. This is background; do not write about ideas to solve problems here.
-->
If we could write a fully reactive application we could easily disable zone.js as we always know when something should get rerendered.
To do this we need an adapted async pipe to include some logic for change detection.
### Prior Art
<!--
What have been done before to adress this problem?
Show the essential way of solving it at the moment. Do not include specific libraries or solutions/ideas. Those things are referenced in the backgrounds resources section.
Demonstrate the minimal way people solve it at the moment.
-->
At the moment change detection can be done manually by calling
- ViewEngine
- in zone-full mode `ChangeDetectorRef.markForCheck`
- in zone-less mode `ChangeDetectorRef.detectChanges`
- Ivy
(listed here to show that things work different under the hood. At the end `ChangeDetectorRef` is used regardless of ivy differences )
- in zone-full mode `ɵmarkDirty`
- in zone-less mode `ɵdetectChanges`
And automatically by using the `async` pipe in the template which is calling `ChangeDetectorRef.markForCheck` in ViewEngine and Ivy (the version of Angular was [9.0.0-rc.13](https://github.com/angular/angular/tree/9.0.0-rc.13) when writing this.
## Prototype
### Minimal Design
```typescript
@Pipe({ name: 'push', pure: false })
export class PushPipe implements PipeTransform {
// property to hold the value that gets renderer to the template
private renderedValue?: unknown;
// subscription to the renderer process
private subscription: Subscription = new Subscription();
constructor(protected cdRef: ChangeDetectorRef) {
}
transform<T>(obs: Observable<T> | Promise<T> | null | undefined): T | null {
// Retreive values from passed argument
// using .add to subscription because if we get a new reference and make a new subscription to it will loose the reference to previous subscription
this.subscription.add(obs.pipe(
tap({
// Assign value that will get returned from the transform function
// on the next change detection
next: (valueToRender: unknown) => {
// Apply values that should get rendered
this.renderedValue = valueToRender;
this.cdRef.detectChanges();
}
})
)
// Start to render passed values
.subscribe());
// Render values from passed argument on every chage detection run
return this.renderedValue;
}
ngOnDestroy() {
// Stop to render values
this.subscription.unsubscribe();
}
}
```
### Detailed Design
The following code includes only the very essence to explain what it does. The real implementation includes some abstractions as well as other additional things and all the configuration logic as well as the renderer and zone related decisions.
```typescript
export function toObservableValue<T>(
potentialObservableValue$: potentialObservableValue<T>
): Observable<T | undefined | null> {
if (isUndefinedOrNullGuard(potentialObservableValue$)) {
return of(potentialObservableValue$);
}
if (
isPromiseGuard(potentialObservableValue$) ||
isObservableGuard(potentialObservableValue$)
) {
return from(potentialObservableValue$);
}
throw new ArgumentNotObservableError();
}
@Pipe({ name: 'push', pure: false })
export class PushPipe implements PipeTransform {
// property to hold the value that gets renderer to the template
private renderedValue: unknown = undefined;
// subscription to the renderer process
private subscription: Subscription = new Subscription();
protected observablesSubject = new Subject<Observable<unknown> | Promise<unknown> | null | undefined>();
protected observables$ = this.observablesSubject.pipe(
// Ensure type of passed value
toObservableValue(),
catchError({ error: e => throwError(ArgumentNotObservableError(e)) }),
// Ignore observables of the same instances
// This is needed to avoid endless loops of change detection
// And also cleans up emitted values to the relevant once
distinctUntilChanged(),
// Logic related to reset value in the view
tap({
next: (obs: Observable<unknown>) => {
// Apply values that should get rendered
this.renderedValue = undefined;
// Render new values to the template
this.cdRef.detectChanges();
}
}),
// @NOTICE:
// Anything configurable over pipe params or other values from the pipes class can be done here
//
// Unsubscribe from previous observables
// Then flatten the latest internal observables into the output
switchAll(), // Observable<Observable<T>> to Observable<T>
// Forward only new pimitive values or new instances
// To reduce number of change detection calls
// @NOTICE:
// We check also for new instances so we force the user to work immutable
distinctUntilChanged(),
// Update renderedValue and render it to the template
tap({
// Assign value that will get returned from the transform function
// on the next change detection
next: (valueToRender: unknown) => {
// Apply values that should get rendered
this.renderedValue = valueToRender;
},
// Get error object here and apply needed error logic
error: (e: unknown) => {
// Logic to deal with error object
// e. g. this.renderedValue = e.message; this.cdRef.detectChanges();
// @Notice:
// This is not catching the error
}
}),
// coalesce changes within LView context
coalesce(() => animationFrames, {context: this.cdRef['lView']}),
tap(() => {
// Render new values to the template
this.cdRef.detectChanges();
})
);
constructor(protected cdRef: ChangeDetectorRef, protected ngZone: NgZone) {
// Start to be render passed values
this.subscription = this.observables$.subscribe();
}
transform<T>(obs: Observable<T> | Promise<T> | null | undefined): T | null | undefined {
// Retreive values from passed argument
this.observablesSubject.next(obs);
// Render values from passed argument on every chage detection run
return this.renderedValue;
}
ngOnDestroy() {
// Stop to render values
this.subscription.unsubscribe();
}
}
```
### Caveats
<!--
You may need to describe what you did not do or why simpler approaches don't work. Mention other things to watch out for (if any).
Security Considerations
How you’ll be secure. Considerations should include sanitization, escaping, existing security protocols and web standards, permission, user privacy, etc.
None
-->
**ViewEngine / Ivy interoperability**
With the requirement to support both, ViewEngine and ivy, we may increase complexity of the implementation.
But currently we rely on Angular APIs of `ChangeDetectionRef.detectChanges` and `ChangeDetectionRef.markForCheck` that work in a similar way on both renders.
**Immutability:**
The second `distinctUntilChanged` operator for emitted values of the passed observables forces the user to work immutable. Even if this is in most cases given as people that
work more reactively use normally also `ChangeDetectionStrategy.OnPush`.
Still, we have to consider clear communication.
**Coalescing and Scoped Coalescing:**
This problem is explained in the document [Design Doc - Coalescing of Change Detection](https://hackmd.io/42oAMrzYReizA65AwQ7ZlQ)
**Sanitization:**
Sanitization of the emitted values to get rendered in the template is done by default by Angular.
### Performance Considerations
<!--
Try to describe under which conditions the suggested solution is performant and which factors play the key role when starting to get bad performance.
Describe a specific situation in which we can run into performance problems
If possible provide a POC or a theoretical explanation of a possible solution.
-->
Edge cases that could be considered.
There are certain situations where the usage of the pipe in the above implementation can lead to performance problems.
Imagine follwing code:
```htmlmixed
{{observable$ | push}}
{{observable$ | push}}
{{observable$ | push}}
```
If we run zone-less this would cause 3 renderings in the same event-loop turn.
To avoid this we need to coalesce the change detection calls.
If we would put `PushPipe` as context the change detection could only be triggered once for the whole application in the same event-loop.
We need the unpatched value as otherwise, we would interfere with zone.js.
Critical to consider here is we can one or multiple `*rxLet` directives are also present in the same template next to one or multiple `push`.
Also that the received `ChangeDetectorRef` is different when using a template scope or property binding.
Details on change detection coalescing feature can be found in [Design Doc - Coalescing of Change Detection](https://hackmd.io/42oAMrzYReizA65AwQ7ZlQ).
## Open questions
n/a
## Documentation Plan
<!--
Try to describe the important parts of the implementation and how to documented it e.g. importance, a priority by relevance for user, level of detail, example needed.
-->
As the scope of this package is quite small I consider the documentation in text and some examples in the demo app and as code snippets
is everything we need.
Here a first draft of the documentation:
[push.md](https://github.com/rx-angular/rx-angular/blob/master/libs/template/docs/push.md)
### Style Guide Impact
<!--
Does the documentation influence the way the current style guide is structured?
Also, does the new documentation introduces any technical implementations of the docs?
If so please name them and give a detailed description of the impact and if possible some POCs.
-->
The needed documentation has no impact on the current way the documentation is maintained.
## Developer Impact
### Public API Surface Impact
<!--
Are you adding new public APIs? If so, what's the change? (method signature changes, new classes/interfaces, new npm packages, config file properties or new config files altogether)
Are the new APIs consistent with the existing APIs? Do they follow naming conventions and use correct terminology?
-->
The new pipe does not affect the existing API interface. It will get introduced under a new package `@rx-angular/template`.
It exposes only the pipes class and its usage in the template as a pipe reference.
Following dependencies are needed:
- @angular/core@>=8
- @rxjs@>=6.5.4
The new pipe is named `push`.
It is used the same way as the `async` pipe is used `{{observable$ | push}}`.
The term 'push' refers to push-based computation like with asynchronous code and callbacks or event listeners.
This tries to give the user an intuitive understanding of its behavior out of the pipe's name itself.
### Developer Experience Impact
<!--
How will this change impact developer experience?
Are we adding new tools developers need to learn? Are we asking developers to change their workflows in any way?
Are we placing any new constraints on how the developer should write the code to be compatible with this change?
-->
This has no impact on a user's workflow but can increase performance and enables them to run zone-less.
If the users decide to disable zone he may need to get more familiar with RxJS operators.
Other than this the user does not need to adopt anything else to use the `push` pipe.
All `async` pipes could be replaced by `push` pipes without breaking anything.
### Breaking Changes
The new pipe will not introduce any breaking changes.
### Rollout Plan
<!--
Are the implementation delays about to negatively affect or delay the release of other features or increase the size.
-->
The pipe and especially its features can be rolled out in the following way:
**Alpha releases of the @rx-angular/template package** and the pipe include all features the async pipe has, to have the pipe ready for a drop-in replacement, as well as the detection depending if zone is present or not.
Shipped Features:
- Take promises or observables, retrieve their values and render the value to the template
- Handling null and undefined values in a clean unified/structured way
- Triggers change-detection differently if `zone.js` is present or not (`detectChanges` or `markForCheck`)
**Beta releases of the @rx-angular/template package** and the pipe include a configuration argument to opt-in the coalescing of change detection calls.
Shipped Features:
- Add config options to the pipe as arguments in the template to opt-in new features
- Coalescing of change detection calls to boost performance
**Rc and official releases of the @rx-angular/template package** and the pipe include all features and the change detection coalescing is opt-out by default.
Alos internal operators and utils related to zone checks and event coalescing can be exposed in this releases.
Shipped Features:
- Coalescing of change detection calls to boost performance is on by default and can get opted-out
### Rollback Plan
<!--
How do you plan to roll back the change if major issues are found?
-->
In the late alpha versions, we introduce the coalescing of change detection calls.
If any critical problems occur in the alpha version we can just not ship further versions.
Until the first beta, we should have to clarify this. This is the moment we introduce the configuration option for the pipe.
The coalescing can be rolled back by removing the option from the config object.
If a major issue is found in RC or even official release we can remove the push pipe from the package as there is no other part relying on the pipe.
There are no implementation details that could delay the release of other packages. Also, the bundle size of other features will not increase by introducing this feature.
### Maintenance Plan
<!--
Explain how this library will be maintained going forward in releases after the initial release.
This includes not only releases of the subject of this document but also respects its dependencies.
Also put commitments on future maintainace here and try to estimat the handover
-->
If we can get rid of the usage of Angulars internal API we have pretty low maintenance costs.
Depending on how long we want to support interrupt ViewEngine and Ivy we have a year to 1,5 to remove the ViewEngine related code.
The rest of the code relies on some pretty common Angular parts from the core package and some RxJS operators creation functions and utils naming: `switchAll, shareReplay, pipe, distinctUntilChanged, filter, of, from, tap, Observable`
`shareReplay` will have a breaking change for out current usage as the configuration object will get removed.
A minor refactoring.
From all the other used operator we only use one where a breaking change can happen which is `tap`.
If Observabls make it in the standard the most probably will have callbacks as arguments instead of an observer object.
But this is also a minor refactoring.
## Alternatives considered
<!--
2 paragraph
Include alternate design ideas you tried out, but didn't continue with them.
List their disadvantages or at least why you did not invest more time in researching them.
**Name**
Rough description auf the case
List different sub path:
- A)
-B)
Drawbacks:
- Filesize
- Performance
- Workflow
- Tooling
- Documentation
- Maintainance
- Breking Changes
-->
**Imperative Code style**
An alternative implementation could be done without RxJS as Angulars `async`pipe is built. I went the Reactive way because
- A) not a lot of operators
-B) Easier to maintain and extend (configuration)
**Change Detection over ApplicationRef.tick()**
An alternative way of triggering the changeDetection would be to call `ApplicationRef.tick()`.
I did not went that way or even tried it out as It would result in a full application render for every cnahge detection.
No other alternatives considered as this is the minimal implementation.
**Change Detection in the Component**
In the component not all Observales values needs to get rendered. Examples can be any background process like polling or refresh clicks that dispatch an action or so.
**Already exixting Packages**
The suggested version is the most primitive implementation.
In the [RFC](https://github.com/ngrx/platform/issues/2052) for this package is listed alternative implementations.
Those implementations either didn't respect the zone-less mode or had additional logic implemented that would go over the features of a [reactive primitives](https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10#reactive-primitives).
## Work Breakdown
<!--
Explain how multiple people would actively working on the suggested code base.
If needed include branching suggestions or the way code interacts
-->
Other than this, as the pipes scope is small enough to get maintained by one person at a time we don't need to describe the breakdown of work.
## Resources
**Research Paper/Design Docs/StackBlitz/Video/Podcast/Blog/Tweet/Graphic:**
- [Reactive Angular Terminology](https://gist.github.com/BioPhoton/e8e650dc3b8a7798d09d3a66916bbe10) In: GitHub Gist, Michael Hladky.
- [RFC: Component: Proposal for a new package component](https://github.com/ngrx/platform/issues/2052) In: GitHub, Michael Hladky.
- [Render3/VIEW_DATA.md](https://github.com/angular/angular/blob/master/packages/core/src/render3/VIEW_DATA.md) In: GitHub, Angular.
- [Design Doc - Coalescing of Change Detection](https://hackmd.io/42oAMrzYReizA65AwQ7ZlQ) In: HackMd, Michael Hladky.
- [Keynote - What's New and Coming in Angular | AngularUP](https://www.youtube.com/watch?v=-32nh-pGXaU&feature=youtu.be&t=19m02s) In: YouTube, Rob Wormald, 2019-07-05.
**Github Pull Request/Gists/Issue/Doc/Source Link:**
- [angular-ivy-detection.ts](https://gist.github.com/LayZeeDK/2cd1ff1fc605af6f578a39311b3aba99)
- [RFC: Component: Evaluation of using angular's internal `ɵdetectChanges` method](https://github.com/ngrx/platform/issues/2050)
- [isPromise check](https://github.com/ReactiveX/rxjs/pull/5291)