Skip to content

Commit 034ce71

Browse files
authored
Bugfix/rx stateful no subs values (#112)
* #111 fix(rx-stateful): fix not emitting subsequent values The issue was caused when a source does not emit a thruthy value. This is e.g. the case for Endpoints which send an empty response back, e.g. after a delete-operation. The problem can easiliy be simulated by ```ts rxStateful$( timer(1000).pipe(switchMap(() => of(null))) ) ``` * chore: ehnance demos
1 parent 44b62de commit 034ce71

File tree

9 files changed

+428
-6
lines changed

9 files changed

+428
-6
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<!--<div>-->
2+
<!-- <h1>Case 1</h1>-->
3+
<!-- <div>-->
4+
<!-- <button (click)="refresh$$.next(null)">refresh</button>-->
5+
<!-- <div>-->
6+
<!-- @if(case1$ | async ; as data){-->
7+
<!-- <rx-stateful-state-visualizer [state]="data"/>-->
8+
<!-- }-->
9+
<!-- </div>-->
10+
<!-- </div>-->
11+
<!--</div>-->
12+
13+
14+
<div>
15+
<h1>Bugfix Reproduction</h1>
16+
<div>
17+
<button (click)="deleteAction$.next(1)">trigger delete</button>
18+
<div>
19+
@if(delete$ | async ; as data){
20+
<rx-stateful-state-visualizer [state]="data"/>
21+
}
22+
</div>
23+
</div>
24+
</div>
25+
26+
27+
28+
29+
<div>
30+
<h1>Bugfix Reproduction Normal Case</h1>
31+
<div>
32+
<button (click)="refresh$.next(null)">trigger refresh</button>
33+
<div>
34+
@if(two$ | async ; as data){
35+
<rx-stateful-state-visualizer [state]="data"/>
36+
}
37+
</div>
38+
</div>
39+
</div>
40+
41+
<demo-non-flicker/>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
button{
2+
padding: 8px 16px;
3+
border-radius: 9999px;
4+
font-weight: bold;
5+
border: 2px solid deepskyblue;
6+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
import { AllUseCasesComponent } from './all-use-cases.component';
3+
4+
describe('AllUseCasesComponent', () => {
5+
let component: AllUseCasesComponent;
6+
let fixture: ComponentFixture<AllUseCasesComponent>;
7+
8+
beforeEach(async () => {
9+
await TestBed.configureTestingModule({
10+
imports: [AllUseCasesComponent],
11+
}).compileComponents();
12+
13+
fixture = TestBed.createComponent(AllUseCasesComponent);
14+
component = fixture.componentInstance;
15+
fixture.detectChanges();
16+
});
17+
18+
it('should create', () => {
19+
expect(component).toBeTruthy();
20+
});
21+
});
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import {Component, inject, Injectable} from '@angular/core';
2+
import { CommonModule } from '@angular/common';
3+
import {HttpClient, HttpErrorResponse} from "@angular/common/http";
4+
import {delay, Observable, of, OperatorFunction, scan, Subject, switchMap, timer} from "rxjs";
5+
import {RxStateful, rxStateful$, withAutoRefetch, withRefetchOnTrigger} from "@angular-kit/rx-stateful";
6+
import {Todo} from "../types";
7+
import {RxStatefulStateVisualizerComponent} from "./rx-stateful-state-visualizer.component";
8+
import {NonFlickerComponent} from "./non-flicker/non-flicker.component";
9+
10+
type Data = {
11+
id: number;
12+
name: string
13+
}
14+
15+
const DATA: Data[] = [
16+
{id: 1, name: 'ahsd'},
17+
{id: 2, name: 'asdffdsa'},
18+
{id: 3, name: 'eeasdf'},
19+
]
20+
21+
@Injectable({providedIn: 'root'})
22+
export class DataService {
23+
private readonly http = inject(HttpClient)
24+
25+
26+
getData(opts?: {delay?: number}){
27+
return timer(opts?.delay ?? 1000).pipe(
28+
switchMap(() => of(DATA))
29+
)
30+
}
31+
32+
getById(id: number, opts?: {delay?: number}){
33+
return timer(opts?.delay ?? 1000).pipe(
34+
switchMap(() => of(DATA.find(v =>v.id === id)))
35+
)
36+
}
37+
}
38+
39+
@Component({
40+
selector: 'demo-all-use-cases',
41+
standalone: true,
42+
imports: [CommonModule, RxStatefulStateVisualizerComponent, NonFlickerComponent],
43+
templateUrl: './all-use-cases.component.html',
44+
styleUrl: './all-use-cases.component.scss',
45+
})
46+
export class AllUseCasesComponent {
47+
private readonly http = inject(HttpClient)
48+
private readonly data = inject(DataService)
49+
readonly refresh$$ = new Subject<null>()
50+
refreshInterval = 10000
51+
/**
52+
* Für alle Use Cases eine demo machen
53+
*/
54+
55+
/**
56+
* Case 1
57+
* Basic Usage with automatic refetch and a refreshtrigger
58+
*/
59+
case1$ = rxStateful$<Data[], Error>(
60+
this.data.getData(),
61+
{
62+
refetchStrategies: [
63+
withRefetchOnTrigger(this.refresh$$),
64+
//withAutoRefetch(this.refreshInterval, 1000000)
65+
],
66+
suspenseThresholdMs: 0,
67+
suspenseTimeMs: 0,
68+
keepValueOnRefresh: false,
69+
keepErrorOnRefresh: false,
70+
errorMappingFn: (error) => error.message,
71+
}
72+
).pipe(
73+
collectState()
74+
)
75+
76+
/**
77+
* Case Basic Usage non flickering
78+
*/
79+
80+
/**
81+
* Case Basic Usage flaky API
82+
*/
83+
//case2$
84+
85+
/**
86+
* Case - sourcetrigger function
87+
*/
88+
89+
90+
/**
91+
* Case - sourcetrigger function non flickering
92+
*/
93+
94+
/**
95+
* Case - sourcetrigger function flaky api
96+
*/
97+
98+
/**
99+
* Case Bug Reproduction https://github.com/mikelgo/angular-kit/issues/111
100+
*/
101+
102+
deleteAction$ = new Subject<number>()
103+
104+
delete$ = rxStateful$(
105+
// id => this.http.get(`https://jsonplaceholder.typicode.com/posts/${id}`),
106+
id => timer(1000).pipe(
107+
switchMap(() => of(null))
108+
),
109+
{
110+
suspenseTimeMs: 0,
111+
suspenseThresholdMs: 0,
112+
sourceTriggerConfig: {
113+
operator: 'switch',
114+
trigger: this.deleteAction$
115+
}
116+
}
117+
).pipe(
118+
collectState()
119+
)
120+
121+
/**
122+
* Case Normal for Bug repro
123+
*/
124+
refresh$ = new Subject<null>()
125+
two$ = rxStateful$(
126+
timer(1000).pipe(
127+
switchMap(() => of(null))
128+
),
129+
{
130+
refetchStrategies: [withRefetchOnTrigger(this.refresh$)]
131+
}
132+
).pipe(
133+
collectState()
134+
)
135+
}
136+
137+
138+
function collectState(): OperatorFunction<RxStateful<any>, {
139+
index: number;
140+
value: RxStateful<any>
141+
}[]>{
142+
return scan<RxStateful<any>, {
143+
index: number;
144+
value: RxStateful<any>
145+
}[]>((acc, value, index) => {
146+
// @ts-ignore
147+
acc.push({ index, value });
148+
149+
return acc;
150+
}, [])
151+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import {Component, inject} from '@angular/core';
2+
import { CommonModule } from '@angular/common';
3+
import {HttpClient} from "@angular/common/http";
4+
import {ActivatedRoute} from "@angular/router";
5+
import {BehaviorSubject, concatAll, delay, map, scan, Subject, switchMap, tap, toArray} from "rxjs";
6+
import {provideRxStatefulClient, RxStatefulClient, withConfig} from "@angular-kit/rx-stateful/experimental";
7+
import {rxStateful$, withRefetchOnTrigger} from "@angular-kit/rx-stateful";
8+
9+
@Component({
10+
selector: 'demo-non-flicker',
11+
standalone: true,
12+
imports: [CommonModule],
13+
template: `
14+
<h1>DemoRxStatefulComponent</h1>
15+
<!-- <div>-->
16+
<!-- <button (click)="refresh$$.next(null)">refresh</button>-->
17+
<!-- </div>-->
18+
<!-- <div>-->
19+
<!-- <h4>State</h4>-->
20+
<!-- <div *ngIf="state$ | async as state">-->
21+
<!-- <div *ngIf="state.value">{{ state.value | json }}</div>-->
22+
<!-- <div *ngIf="state.isSuspense">loading</div>-->
23+
<!-- </div>-->
24+
<!-- </div>-->
25+
<!-- <div>-->
26+
<!-- <h4>State Accumulated</h4>-->
27+
<!-- <ul *ngFor="let v of stateAccumulated$ | async">-->
28+
<!-- <li>{{ v | json }}</li>-->
29+
<!-- </ul>-->
30+
<!-- </div>-->
31+
<!-- <div>-->
32+
<!-- <h4>Query Params</h4>-->
33+
<!-- <div>{{ query$ | async | json }}</div>-->
34+
<!-- <div>{{ value$ | async | json }}</div>-->
35+
<!-- </div>-->
36+
37+
<!-- <br>-->
38+
<div>
39+
<button mat-button color="primary" (click)="page$$.next(-1)"> previous page </button>
40+
<button mat-button color="primary" (click)="page$$.next(1)"> next page </button>
41+
<button mat-button color="primary" (click)="refresh$$.next(null)"> Refresh current page </button>
42+
<div>
43+
<h4>State Accumulated</h4>
44+
<ul *ngFor="let v of state2Accumulated$ | async">
45+
<li>{{ v | json }}</li>
46+
</ul>
47+
</div>
48+
</div>
49+
`,
50+
styles: `
51+
:host {
52+
display: block;
53+
}
54+
`,
55+
providers: [
56+
provideRxStatefulClient(
57+
withConfig({ keepValueOnRefresh: false, errorMappingFn: (e) => e})
58+
),
59+
// provideRxStatefulConfig({keepValueOnRefresh: true, errorMappingFn: (e) => e})
60+
],
61+
})
62+
export class NonFlickerComponent {
63+
private http = inject(HttpClient);
64+
private route = inject(ActivatedRoute);
65+
refresh$$ = new Subject<any>();
66+
67+
client = inject(RxStatefulClient);
68+
69+
query$ = this.route.params;
70+
71+
value$ = this.query$.pipe(switchMap(() => this.client.request(this.fetch()).pipe(
72+
map(v => v.value)
73+
)));
74+
75+
// instance = this.client.request(this.fetch(), {
76+
// keepValueOnRefresh: false,
77+
// keepErrorOnRefresh: false,
78+
// refreshTrigger$: this.refresh$$,
79+
// refetchStrategies: [withAutoRefetch(10000, 20000)],
80+
// });
81+
// state$ = this.instance;
82+
// stateAccumulated$ = this.state$.pipe(
83+
// tap(console.log),
84+
// scan((acc, value, index) => {
85+
// @ts-ignore
86+
// acc.push({ index, value });
87+
//
88+
// return acc;
89+
// }, [])
90+
// );
91+
92+
93+
state$ = rxStateful$(this.fetch(450), {
94+
keepValueOnRefresh: false,
95+
keepErrorOnRefresh: false,
96+
refreshTrigger$: this.refresh$$,
97+
suspenseTimeMs: 3000,
98+
suspenseThresholdMs: 500
99+
});
100+
101+
stateAccumulated$ = this.state$.pipe(
102+
tap(x => console.log({state: x})),
103+
scan((acc, value, index) => {
104+
// @ts-ignore
105+
acc.push({ index, value });
106+
107+
return acc;
108+
}, [])
109+
);
110+
readonly page$$ = new BehaviorSubject(0)
111+
readonly page$ = this.page$$.pipe(
112+
scan((acc, curr) => acc + curr, 0)
113+
)
114+
115+
state2$ = rxStateful$(
116+
(page) => this.fetchPage({
117+
page,
118+
delayInMs: 5000
119+
}).pipe(
120+
121+
),
122+
{
123+
suspenseThresholdMs: 500,
124+
suspenseTimeMs: 2000,
125+
sourceTriggerConfig: {
126+
trigger: this.page$
127+
},
128+
refetchStrategies: withRefetchOnTrigger(this.refresh$$)
129+
}
130+
)
131+
state2Accumulated$ = this.state2$.pipe(
132+
tap(x => console.log({state: x})),
133+
scan((acc, value, index) => {
134+
// @ts-ignore
135+
acc.push({ index, value });
136+
137+
return acc;
138+
}, [])
139+
);
140+
141+
fetch(delayInMs = 800) {
142+
return this.http.get<any>('https://jsonplaceholder.typicode.com/todos/1').pipe(
143+
delay(delayInMs),
144+
map((v) => v?.title),
145+
// tap(console.log)
146+
);
147+
}
148+
149+
fetchPage(params: {
150+
delayInMs:number,
151+
page: number
152+
}) {
153+
154+
return this.http.get<any>(`https://jsonplaceholder.typicode.com/todos?_start=${params.page * 5}&_limit=5`).pipe(
155+
delay(params.delayInMs),
156+
concatAll(),
157+
// @ts-ignore
158+
map((v) => v?.id),
159+
toArray()
160+
);
161+
}
162+
163+
constructor() {
164+
this.state$.subscribe();
165+
this.state$.subscribe();
166+
}
167+
}

0 commit comments

Comments
 (0)