-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
44b62de
commit 034ce71
Showing
9 changed files
with
428 additions
and
6 deletions.
There are no files selected for viewing
41 changes: 41 additions & 0 deletions
41
apps/demo-rx-stateful/src/app/all-use-cases/all-use-cases.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
<!--<div>--> | ||
<!-- <h1>Case 1</h1>--> | ||
<!-- <div>--> | ||
<!-- <button (click)="refresh$$.next(null)">refresh</button>--> | ||
<!-- <div>--> | ||
<!-- @if(case1$ | async ; as data){--> | ||
<!-- <rx-stateful-state-visualizer [state]="data"/>--> | ||
<!-- }--> | ||
<!-- </div>--> | ||
<!-- </div>--> | ||
<!--</div>--> | ||
|
||
|
||
<div> | ||
<h1>Bugfix Reproduction</h1> | ||
<div> | ||
<button (click)="deleteAction$.next(1)">trigger delete</button> | ||
<div> | ||
@if(delete$ | async ; as data){ | ||
<rx-stateful-state-visualizer [state]="data"/> | ||
} | ||
</div> | ||
</div> | ||
</div> | ||
|
||
|
||
|
||
|
||
<div> | ||
<h1>Bugfix Reproduction Normal Case</h1> | ||
<div> | ||
<button (click)="refresh$.next(null)">trigger refresh</button> | ||
<div> | ||
@if(two$ | async ; as data){ | ||
<rx-stateful-state-visualizer [state]="data"/> | ||
} | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<demo-non-flicker/> |
6 changes: 6 additions & 0 deletions
6
apps/demo-rx-stateful/src/app/all-use-cases/all-use-cases.component.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
button{ | ||
padding: 8px 16px; | ||
border-radius: 9999px; | ||
font-weight: bold; | ||
border: 2px solid deepskyblue; | ||
} |
21 changes: 21 additions & 0 deletions
21
apps/demo-rx-stateful/src/app/all-use-cases/all-use-cases.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||
import { AllUseCasesComponent } from './all-use-cases.component'; | ||
|
||
describe('AllUseCasesComponent', () => { | ||
let component: AllUseCasesComponent; | ||
let fixture: ComponentFixture<AllUseCasesComponent>; | ||
|
||
beforeEach(async () => { | ||
await TestBed.configureTestingModule({ | ||
imports: [AllUseCasesComponent], | ||
}).compileComponents(); | ||
|
||
fixture = TestBed.createComponent(AllUseCasesComponent); | ||
component = fixture.componentInstance; | ||
fixture.detectChanges(); | ||
}); | ||
|
||
it('should create', () => { | ||
expect(component).toBeTruthy(); | ||
}); | ||
}); |
151 changes: 151 additions & 0 deletions
151
apps/demo-rx-stateful/src/app/all-use-cases/all-use-cases.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import {Component, inject, Injectable} from '@angular/core'; | ||
import { CommonModule } from '@angular/common'; | ||
import {HttpClient, HttpErrorResponse} from "@angular/common/http"; | ||
import {delay, Observable, of, OperatorFunction, scan, Subject, switchMap, timer} from "rxjs"; | ||
import {RxStateful, rxStateful$, withAutoRefetch, withRefetchOnTrigger} from "@angular-kit/rx-stateful"; | ||
import {Todo} from "../types"; | ||
import {RxStatefulStateVisualizerComponent} from "./rx-stateful-state-visualizer.component"; | ||
import {NonFlickerComponent} from "./non-flicker/non-flicker.component"; | ||
|
||
type Data = { | ||
id: number; | ||
name: string | ||
} | ||
|
||
const DATA: Data[] = [ | ||
{id: 1, name: 'ahsd'}, | ||
{id: 2, name: 'asdffdsa'}, | ||
{id: 3, name: 'eeasdf'}, | ||
] | ||
|
||
@Injectable({providedIn: 'root'}) | ||
export class DataService { | ||
private readonly http = inject(HttpClient) | ||
|
||
|
||
getData(opts?: {delay?: number}){ | ||
return timer(opts?.delay ?? 1000).pipe( | ||
switchMap(() => of(DATA)) | ||
) | ||
} | ||
|
||
getById(id: number, opts?: {delay?: number}){ | ||
return timer(opts?.delay ?? 1000).pipe( | ||
switchMap(() => of(DATA.find(v =>v.id === id))) | ||
) | ||
} | ||
} | ||
|
||
@Component({ | ||
selector: 'demo-all-use-cases', | ||
standalone: true, | ||
imports: [CommonModule, RxStatefulStateVisualizerComponent, NonFlickerComponent], | ||
templateUrl: './all-use-cases.component.html', | ||
styleUrl: './all-use-cases.component.scss', | ||
}) | ||
export class AllUseCasesComponent { | ||
private readonly http = inject(HttpClient) | ||
private readonly data = inject(DataService) | ||
readonly refresh$$ = new Subject<null>() | ||
refreshInterval = 10000 | ||
/** | ||
* Für alle Use Cases eine demo machen | ||
*/ | ||
|
||
/** | ||
* Case 1 | ||
* Basic Usage with automatic refetch and a refreshtrigger | ||
*/ | ||
case1$ = rxStateful$<Data[], Error>( | ||
this.data.getData(), | ||
{ | ||
refetchStrategies: [ | ||
withRefetchOnTrigger(this.refresh$$), | ||
//withAutoRefetch(this.refreshInterval, 1000000) | ||
], | ||
suspenseThresholdMs: 0, | ||
suspenseTimeMs: 0, | ||
keepValueOnRefresh: false, | ||
keepErrorOnRefresh: false, | ||
errorMappingFn: (error) => error.message, | ||
} | ||
).pipe( | ||
collectState() | ||
) | ||
|
||
/** | ||
* Case Basic Usage non flickering | ||
*/ | ||
|
||
/** | ||
* Case Basic Usage flaky API | ||
*/ | ||
//case2$ | ||
|
||
/** | ||
* Case - sourcetrigger function | ||
*/ | ||
|
||
|
||
/** | ||
* Case - sourcetrigger function non flickering | ||
*/ | ||
|
||
/** | ||
* Case - sourcetrigger function flaky api | ||
*/ | ||
|
||
/** | ||
* Case Bug Reproduction https://github.com/mikelgo/angular-kit/issues/111 | ||
*/ | ||
|
||
deleteAction$ = new Subject<number>() | ||
|
||
delete$ = rxStateful$( | ||
// id => this.http.get(`https://jsonplaceholder.typicode.com/posts/${id}`), | ||
id => timer(1000).pipe( | ||
switchMap(() => of(null)) | ||
), | ||
{ | ||
suspenseTimeMs: 0, | ||
suspenseThresholdMs: 0, | ||
sourceTriggerConfig: { | ||
operator: 'switch', | ||
trigger: this.deleteAction$ | ||
} | ||
} | ||
).pipe( | ||
collectState() | ||
) | ||
|
||
/** | ||
* Case Normal for Bug repro | ||
*/ | ||
refresh$ = new Subject<null>() | ||
two$ = rxStateful$( | ||
timer(1000).pipe( | ||
switchMap(() => of(null)) | ||
), | ||
{ | ||
refetchStrategies: [withRefetchOnTrigger(this.refresh$)] | ||
} | ||
).pipe( | ||
collectState() | ||
) | ||
} | ||
|
||
|
||
function collectState(): OperatorFunction<RxStateful<any>, { | ||
index: number; | ||
value: RxStateful<any> | ||
}[]>{ | ||
return scan<RxStateful<any>, { | ||
index: number; | ||
value: RxStateful<any> | ||
}[]>((acc, value, index) => { | ||
// @ts-ignore | ||
acc.push({ index, value }); | ||
|
||
return acc; | ||
}, []) | ||
} |
167 changes: 167 additions & 0 deletions
167
apps/demo-rx-stateful/src/app/all-use-cases/non-flicker/non-flicker.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import {Component, inject} from '@angular/core'; | ||
import { CommonModule } from '@angular/common'; | ||
import {HttpClient} from "@angular/common/http"; | ||
import {ActivatedRoute} from "@angular/router"; | ||
import {BehaviorSubject, concatAll, delay, map, scan, Subject, switchMap, tap, toArray} from "rxjs"; | ||
import {provideRxStatefulClient, RxStatefulClient, withConfig} from "@angular-kit/rx-stateful/experimental"; | ||
import {rxStateful$, withRefetchOnTrigger} from "@angular-kit/rx-stateful"; | ||
|
||
@Component({ | ||
selector: 'demo-non-flicker', | ||
standalone: true, | ||
imports: [CommonModule], | ||
template: ` | ||
<h1>DemoRxStatefulComponent</h1> | ||
<!-- <div>--> | ||
<!-- <button (click)="refresh$$.next(null)">refresh</button>--> | ||
<!-- </div>--> | ||
<!-- <div>--> | ||
<!-- <h4>State</h4>--> | ||
<!-- <div *ngIf="state$ | async as state">--> | ||
<!-- <div *ngIf="state.value">{{ state.value | json }}</div>--> | ||
<!-- <div *ngIf="state.isSuspense">loading</div>--> | ||
<!-- </div>--> | ||
<!-- </div>--> | ||
<!-- <div>--> | ||
<!-- <h4>State Accumulated</h4>--> | ||
<!-- <ul *ngFor="let v of stateAccumulated$ | async">--> | ||
<!-- <li>{{ v | json }}</li>--> | ||
<!-- </ul>--> | ||
<!-- </div>--> | ||
<!-- <div>--> | ||
<!-- <h4>Query Params</h4>--> | ||
<!-- <div>{{ query$ | async | json }}</div>--> | ||
<!-- <div>{{ value$ | async | json }}</div>--> | ||
<!-- </div>--> | ||
<!-- <br>--> | ||
<div> | ||
<button mat-button color="primary" (click)="page$$.next(-1)"> previous page </button> | ||
<button mat-button color="primary" (click)="page$$.next(1)"> next page </button> | ||
<button mat-button color="primary" (click)="refresh$$.next(null)"> Refresh current page </button> | ||
<div> | ||
<h4>State Accumulated</h4> | ||
<ul *ngFor="let v of state2Accumulated$ | async"> | ||
<li>{{ v | json }}</li> | ||
</ul> | ||
</div> | ||
</div> | ||
`, | ||
styles: ` | ||
:host { | ||
display: block; | ||
} | ||
`, | ||
providers: [ | ||
provideRxStatefulClient( | ||
withConfig({ keepValueOnRefresh: false, errorMappingFn: (e) => e}) | ||
), | ||
// provideRxStatefulConfig({keepValueOnRefresh: true, errorMappingFn: (e) => e}) | ||
], | ||
}) | ||
export class NonFlickerComponent { | ||
private http = inject(HttpClient); | ||
private route = inject(ActivatedRoute); | ||
refresh$$ = new Subject<any>(); | ||
|
||
client = inject(RxStatefulClient); | ||
|
||
query$ = this.route.params; | ||
|
||
value$ = this.query$.pipe(switchMap(() => this.client.request(this.fetch()).pipe( | ||
map(v => v.value) | ||
))); | ||
|
||
// instance = this.client.request(this.fetch(), { | ||
// keepValueOnRefresh: false, | ||
// keepErrorOnRefresh: false, | ||
// refreshTrigger$: this.refresh$$, | ||
// refetchStrategies: [withAutoRefetch(10000, 20000)], | ||
// }); | ||
// state$ = this.instance; | ||
// stateAccumulated$ = this.state$.pipe( | ||
// tap(console.log), | ||
// scan((acc, value, index) => { | ||
// @ts-ignore | ||
// acc.push({ index, value }); | ||
// | ||
// return acc; | ||
// }, []) | ||
// ); | ||
|
||
|
||
state$ = rxStateful$(this.fetch(450), { | ||
keepValueOnRefresh: false, | ||
keepErrorOnRefresh: false, | ||
refreshTrigger$: this.refresh$$, | ||
suspenseTimeMs: 3000, | ||
suspenseThresholdMs: 500 | ||
}); | ||
|
||
stateAccumulated$ = this.state$.pipe( | ||
tap(x => console.log({state: x})), | ||
scan((acc, value, index) => { | ||
// @ts-ignore | ||
acc.push({ index, value }); | ||
|
||
return acc; | ||
}, []) | ||
); | ||
readonly page$$ = new BehaviorSubject(0) | ||
readonly page$ = this.page$$.pipe( | ||
scan((acc, curr) => acc + curr, 0) | ||
) | ||
|
||
state2$ = rxStateful$( | ||
(page) => this.fetchPage({ | ||
page, | ||
delayInMs: 5000 | ||
}).pipe( | ||
|
||
), | ||
{ | ||
suspenseThresholdMs: 500, | ||
suspenseTimeMs: 2000, | ||
sourceTriggerConfig: { | ||
trigger: this.page$ | ||
}, | ||
refetchStrategies: withRefetchOnTrigger(this.refresh$$) | ||
} | ||
) | ||
state2Accumulated$ = this.state2$.pipe( | ||
tap(x => console.log({state: x})), | ||
scan((acc, value, index) => { | ||
// @ts-ignore | ||
acc.push({ index, value }); | ||
|
||
return acc; | ||
}, []) | ||
); | ||
|
||
fetch(delayInMs = 800) { | ||
return this.http.get<any>('https://jsonplaceholder.typicode.com/todos/1').pipe( | ||
delay(delayInMs), | ||
map((v) => v?.title), | ||
// tap(console.log) | ||
); | ||
} | ||
|
||
fetchPage(params: { | ||
delayInMs:number, | ||
page: number | ||
}) { | ||
|
||
return this.http.get<any>(`https://jsonplaceholder.typicode.com/todos?_start=${params.page * 5}&_limit=5`).pipe( | ||
delay(params.delayInMs), | ||
concatAll(), | ||
// @ts-ignore | ||
map((v) => v?.id), | ||
toArray() | ||
); | ||
} | ||
|
||
constructor() { | ||
this.state$.subscribe(); | ||
this.state$.subscribe(); | ||
} | ||
} |
Oops, something went wrong.