Commit a5d1fe1f authored by micha's avatar micha
Browse files

progress zip

parent bf83651d
......@@ -52,19 +52,17 @@ Exercises we will master are:
- State aggregation
- Model vs ViewModel
- Sharing state and instances
- Lazy-ness and component initialization
- Lazyness and component initialization
- Immutability and gotchas in operators
- Higher-Order Operators
- Overview of the different flattening strategies
(merge, concat, exhaust, switch)
- Usage in the user Interface
- Usage in business logic or data layer interaction like effects/epic of REDUX
- Overview of the different flattening operators
- Avoid race-conditions and over-fetching in UIs
- HTTP optimization strategies
- Resilience and Error-Handling
- HTTP error handling
- In-depth understanding of error handling
- Error handling operators overview
- How to encapsulate error-prone code
- Offline caching and polling with exponential backoff
- Load balancing and polling
- Offline friendly and exponential backoff
- Comparison of the different retry & repeat mechanisms
Let’s jump right in and start with the first section.
......@@ -82,11 +82,11 @@ Within this set of lessons we will walk through the following exercises:
- [ ] As we go we start to introduce more features into our UI
- again we run into a problem, this time over-rendering.
- zip
- [ ] With a fresh and open mind, we think about those concepts in combination with a UX Pattern called `opt-in updates`
- to give a better experience to our users we implement this pattern in our example
- [ ] As it was quite technical so far we learn about `withLatestFrom` with a more playful example
- by doing so we understand the concept of `primary` and `secondary` streams
- withLatestFrom
- [ ] With a fresh and open mind, we think about those concepts in combination with a UX Pattern called `opt-in updates`
- to give a better experience to our users we implement this pattern in our example
......@@ -14,9 +14,9 @@ import {CombineLatestBlogService} from "./combine-latest-blog.service";
</mat-form-field>
<button mat-raised-button color="primary">Add Post</button>
<div *ngIf="blog$ | async as list">
<div *ngIf=" as list">
<mat-list>
<mat-list-item *ngFor="let item of list">
<mat-list-item *ngFor="let item of blog$ | async">
<span mat-line>{{item.title}}</span>
<span mat-line>Comments: {{item.commentCount}}</span>
</mat-list-item>
......
# Processing dependent values - Exercise
![](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-process-dependent-values-with-zip_michael-hladky.png)
## Intro
Our project got some updates, and we now display 2 lists.
On the left all blog post and on the right only the blog post with comments:
```html
<div class="row">
<div style="width: 49%" *ngIf="blog$ | async as list">
<b>All items</b>
<mat-list>
<mat-list-item *ngFor="let item of list">
{{item.title}} - Comments: {{item.commentCount}}
</mat-list-item>
</mat-list>
</div>
<div style="width: 49%" *ngIf="commentedPosts$ | async as likedItems">
<b>Liked items</b>
<mat-list>
<mat-list-item *ngFor="let item of likedItems">
{{item.title}} - Comments: {{item.commentCount}}
</mat-list-item>
</mat-list>
</div>
</div>
```
We are asked to improve the number of processing in this component and introduce counter properties in our class to enumerate the rendering.
```Typescript
// Counter properties
numProcessJoinedList = 0;
numRenders = 0;
numProcessLikedList = 0;
// All blog posts
blog$ = combineLatest([
this.blogPostService.posts$,
this.blogPostService.comments$
]).pipe(
map(([list, items]) => toBlogPosts(list, items)),
tap(v => ++this.numProcessJoinedList)
);
commentedIds$ = this.blog$.pipe(
map(list => list
.filter(item => item.commentCount > 0)
.map(item => item.id)
)
);
// Only commented blog posts
commentedPosts$: Observable<BlogPost[]> = combineLatest([
this.blog$,
this.commentedIds$
])
.pipe(
map(([mergedList, likedIds]) => (mergedList.filter(i => likedIds.find(li => li === i.id)))),
tap(v => ++this.numProcessLikedList)
);
constructor(public blogPostService: ZipBlogService) {
this.blogPostService.fetchPosts();
this.blogPostService.fetchComments();
}
```
To get a cleaner picture of is happening we also render the values in the component:
```typescript
@Component({
selector: 'zip',
template: `
...
<p><b>renders: {{renders()}}</b></p>
<p><b>processJoinedList: {{processJoinedList()}}</b></p>
<p><b>processLikedList: {{processLikedList()}}</b></p>
...
`,
})
export class StartZipComponent {
numProcessJoinedList = 0;
numRenders = 0;
numProcessLikedList = 0;
processJoinedList() {
return this.numProcessJoinedList;
}
renders() {
return ++this.numRenders;
}
processLikedList() {
return this.numProcessLikedList;
}
}
```
When clicking on the add button we realize the component is over-rendering, as the numbers are increasing un-proportionally to the data we receive.
![](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators_over-rendering-with-combineLatest_michael-hladky.png)
![](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators_process-dependent-values_michael-hladky.png)
![](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-dependent-values_michael-hladky.png)
## Exercise
Try to implement operators that filter out values which should not get processed. e.g. empty arrays are not interesting to process.
also, if multiple subscriptions get done on the same Observable we could try to share the processed result with all subscribers to reduce the number of processing.
Also consider where and if `zip` could help here.
# Processing dependent values - Solution
As a measure we take the:
- Numbers of processing for bootstrapping
- Numbers of processing for new data
Initial measure:
**Numbers of processing for bootstrapping:**
renders: 6
processJoinedList: 27
processLikedList: 17
**Numbers of processing for new data:**
renders: 8 (Δ2)
processJoinedList: 33 (Δ6)
processLikedList: 21 (Δ4)
As a first step to the solution we introduced filter operators that
swallow empty arrays caused by non-lazy state management to improve the numbers.
```typescript
blogs$ = combineLatest([
this.blogPostService.posts$.pipe(filter(list => !!list.length)),
this.blogPostService.comments$.pipe(filter(list => !!list.length))
])
```
**Numbers of processing for bootstrapping:**
renders: 5 (-1)
processJoinedList: 15 (-12)
processLikedList: 9 (-8)
**Numbers of processing for new data:**
renders: 7 (Δ2 => ~)
processJoinedList: 21 (Δ6 => ~)
processLikedList: 13 (Δ4 => ~)
As `blogs$` is used multiple times we share the processed values over the `share` operator.
```typescript
blogs$ = combineLatest([
this.blogPostService.posts$.pipe(filter(list => !!list.length)),
this.blogPostService.comments$.pipe(filter(list => !!list.length))
])
.pipe(
map(([list, items]) => toBlogPosts(list, items)),
tap(v => ++this.numProcessJoinedList),
share()
);
```
**Numbers of processing for bootstrapping:**
renders: 5 (-1)
processJoinedList: 5 (-22)
processLikedList: 9 (-8)
**Numbers of processing for new data:**
renders: 7 (Δ2 => ~)
processJoinedList: 7 (Δ2 => -4)
processLikedList: 13 (Δ4 => ~)
These improvements didn't change the way of processing it but still gave us a huge performance boost.
Another thing we could think of is to analyze the relations of the processed Observables.
We see that `blogPosts$` hast a relation to `commentedIds$`, or in other words `commentedIds$` is a derivation of `blogPosts$`.
![](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-dependent-values_michael-hladky.png)
`commentedBlogPosts$` needs to process `blogPosts$` and `commentedIds$` in pairs. This helps to avoid irrelevant processing.
![](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators_over-rendering-with-combineLatest_michael-hladky.png)
![](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators_process-dependent-values_michael-hladky.png)
Let's implement `zip` and see the new numbers.
```typescript
commentedBlogPosts$: Observable<BlogPost[]> = zip(
this.blogPosts$,
this.commentedIds$
)
.pipe(
map(([mergedList, likedIds]) => (mergedList.filter(i => likedIds.find(li => li === i.id)))),
tap(v => ++this.numProcessLikedList)
);
```
**Numbers of processing for bootstrapping:**
renders: 5 (-1)
processJoinedList: 5 (-22)
processLikedList: 5 (-12)
**Numbers of processing for new data:**
renders: 7 (Δ2 => ~)
processJoinedList: 7 (Δ2 => -4)
processLikedList: 7 (Δ2 => -2)
Pretty good! :D
## Conclusion
We could get rid of most of the emissions by filtering out empty values and using share.
The last tweaks where done by understanding the data structure relations
e.g. `commentedIds$` is a derivation of `blogPosts$`.
This interesting fact opens a new chapter for us, managing data structures and derivations.
In further exercises we will understand those concepts and avoid the targeted problem of over-rendering id a better more scalable and productive way.
# zip behavior and gotchas
So far the discussed operators where always combining independent Observables,
and the processing get either done for each once of focusing on a primary stream.
`zip` is different here.
## Behavior
`zip` waits for every value of each involved Observable and forwards
one emission for all incoming emissions, meaning it emits one time all first emissions together,
one time all second emissions together and so on and so for.
If values take longer than others it waits with the emission.
Also, if one stream is emitting faster than the other it is waiting with emissions and caches the emitted values until other included streams emitted the same number of times to emit them together.
An example where the emissions wait for their other related Observables could be two polling mechanisms that depend on each other.
In this example we use random intervals to demonstrate this:
```Typescript
import {interval, zip} from 'rxjs';
const input1$ = interval(500); // emission rate varying between 1000 and 3000 ms
const input2$ = interval(1000);
const result$ = zip(input1$, input2$);
result$.subscribe(
([input1, input2]) => console.log(input1, input2)
);
// logs all first, second, third values together: 1 1, 2 2, 3 3, 4 4, 5 5, 6 6
```
As we can see the numbers get logged in pairs and in the right order.
If one stream is faster than the other, the values of the faster one get cached and emitted when its related values arrive.
Here a visual representation of the above example:
![zip - different rates](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-zip-different-rates_michael-hladky.png)
_zip - different rates_
Also, for completely random emission rates zip always emits in the right pairs.
![zip - inner ongoing](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-zip-inner-ongoing_michael-hladky.png)
_zip - inner ongoing_
Errors get forwarded as with all other combination operators. Same btw, is valid for the sibling operator `zipWith`.
![zip - inner error](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-zip-inner-error_michael-hladky.png)
_zip - inner error_
A completion event of one Observable causes the operatro to internally wait for all missing pair values and then completes.
![zip - inner complete](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-zip-inner-complete_michael-hladky.png)
_zip - inner complete_
## 💡 Gotcha(s)!
Be aware that `zip` can buit up a huge cache if the emission rate is too different.
Also, if one of them never emits you have a memory leak.
![zip - never emits if one source never emits](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-zip-never-emits_michael-hladky.png)
_zip - never emits if one source never emits_
![zip - EMPTY results in immediate completion](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-zip-never-emits_michael-hladky.png)
If on of the observables complete without any emission the operator also completes and unsubscribes from all other included Observables internally.
![zip - EMPTY results in immediate completion](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-zip-empty-completes_michael-hladky.png)
_zip - EMPTY results in immediate completion_
import {Component} from '@angular/core';
import {combineLatest, Observable,} from 'rxjs';
import {map, share, shareReplay, tap} from 'rxjs/operators';
import {combineLatest, Observable, zip,} from 'rxjs';
import {distinctUntilChanged, filter, map, share, shareReplay, tap} from 'rxjs/operators';
import {BlogPost, toBlogPosts} from 'shared';
import {ZipBlogService} from "combining-streams/lib/exercises/zip/zip-blog-post.service";
......@@ -20,7 +20,7 @@ import {ZipBlogService} from "combining-streams/lib/exercises/zip/zip-blog-post.
<p><b>processLikedList: {{processLikedList()}}</b></p>
<div class="row">
<div style="width: 49%" *ngIf="blog$ | async as list">
<div style="width: 49%" *ngIf="blogPosts$ | async as list">
<b>All items</b>
<mat-list>
<mat-list-item *ngFor="let item of list">
......@@ -30,7 +30,7 @@ import {ZipBlogService} from "combining-streams/lib/exercises/zip/zip-blog-post.
</div>
<div style="width: 49%" *ngIf="commentedPosts$ | async as likedItems">
<div style="width: 49%" *ngIf="commentedBlogPosts$ | async as likedItems">
<b>Liked items</b>
<mat-list>
<mat-list-item *ngFor="let item of likedItems">
......@@ -55,23 +55,28 @@ export class SolutionZipComponent {
numRenders = 0;
numProcessLikedList = 0;
blog$ = combineLatest([
this.blogPostService.posts$,
this.blogPostService.comments$
blogPosts$ = combineLatest([
this.blogPostService.posts$.pipe(filter(list => !!list.length)),
this.blogPostService.comments$.pipe(filter(list => !!list.length))
]).pipe(
map(([list, items]) => toBlogPosts(list, items)),
tap(v => ++this.numProcessJoinedList)
tap(v => ++this.numProcessJoinedList),
share()
);
commentedIds$ = this.blog$.pipe(map(list => list
.filter(i => i.commentCount > 0)
.map(i => i.id))
commentedIds$ = this.blogPosts$.pipe(
map(list => list
.filter(item => item.commentCount > 0)
.map(item => item.id)
),
distinctUntilChanged()
);
commentedPosts$: Observable<BlogPost[]> = combineLatest([
this.blog$,
//
commentedBlogPosts$: Observable<BlogPost[]> = zip(
this.blogPosts$,
this.commentedIds$
])
)
.pipe(
map(([mergedList, likedIds]) => (mergedList.filter(i => likedIds.find(li => li === i.id)))),
tap(v => ++this.numProcessLikedList)
......
......@@ -20,7 +20,7 @@ import {ZipBlogService} from "combining-streams/lib/exercises/zip/zip-blog-post.
<p><b>processLikedList: {{processLikedList()}}</b></p>
<div class="row">
<div style="width: 49%" *ngIf="blog$ | async as list">
<div style="width: 49%" *ngIf="blogPosts$ | async as list">
<b>All items</b>
<mat-list>
<mat-list-item *ngFor="let item of list">
......@@ -30,7 +30,7 @@ import {ZipBlogService} from "combining-streams/lib/exercises/zip/zip-blog-post.
</div>
<div style="width: 49%" *ngIf="commentedPosts$ | async as likedItems">
<div style="width: 49%" *ngIf="commentedBlogPosts$ | async as likedItems">
<b>Liked items</b>
<mat-list>
<mat-list-item *ngFor="let item of likedItems">
......@@ -55,26 +55,25 @@ export class StartZipComponent {
numRenders = 0;
numProcessLikedList = 0;
blog$ = combineLatest([
this.blogPostService.posts$,
this.blogPostService.comments$
blogPosts$ = combineLatest([
this.blogPostService.posts$.pipe(filter(list => !!list.length)),
this.blogPostService.comments$.pipe(filter(list => !!list.length))
]).pipe(
map(([list, items]) => toBlogPosts(list, items)),
tap(v => ++this.numProcessJoinedList),
share()
);
commentedIds$ = this.blog$.pipe(
commentedIds$ = this.blogPosts$.pipe(
map(list => list
.filter(i => i.commentCount > 0)
.map(i => i.id)
),
distinctUntilChanged()
.filter(item => item.commentCount > 0)
.map(item => item.id)
)
);
//
commentedPosts$: Observable<BlogPost[]> = zip(
this.blog$,
commentedBlogPosts$: Observable<BlogPost[]> = zip(
this.blogPosts$,
this.commentedIds$
)
.pipe(
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment