Skip to content

Commit

Permalink
Bugfix/rx stateful no subs values (#112)
Browse files Browse the repository at this point in the history
* #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
michaelbe812 authored Jul 26, 2024
1 parent 44b62de commit 034ce71
Show file tree
Hide file tree
Showing 9 changed files with 428 additions and 6 deletions.
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/>
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;
}
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 apps/demo-rx-stateful/src/app/all-use-cases/all-use-cases.component.ts
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;
}, [])
}
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();
}
}
Loading

0 comments on commit 034ce71

Please sign in to comment.