diff --git a/apps/demo-rx-stateful/src/app/all-use-cases/all-use-cases.component.html b/apps/demo-rx-stateful/src/app/all-use-cases/all-use-cases.component.html new file mode 100644 index 0000000..934953f --- /dev/null +++ b/apps/demo-rx-stateful/src/app/all-use-cases/all-use-cases.component.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + + +
+

Bugfix Reproduction

+
+ +
+ @if(delete$ | async ; as data){ + + } +
+
+
+ + + + +
+

Bugfix Reproduction Normal Case

+
+ +
+ @if(two$ | async ; as data){ + + } +
+
+
+ + diff --git a/apps/demo-rx-stateful/src/app/all-use-cases/all-use-cases.component.scss b/apps/demo-rx-stateful/src/app/all-use-cases/all-use-cases.component.scss new file mode 100644 index 0000000..a50e10a --- /dev/null +++ b/apps/demo-rx-stateful/src/app/all-use-cases/all-use-cases.component.scss @@ -0,0 +1,6 @@ +button{ + padding: 8px 16px; + border-radius: 9999px; + font-weight: bold; + border: 2px solid deepskyblue; +} diff --git a/apps/demo-rx-stateful/src/app/all-use-cases/all-use-cases.component.spec.ts b/apps/demo-rx-stateful/src/app/all-use-cases/all-use-cases.component.spec.ts new file mode 100644 index 0000000..99016a4 --- /dev/null +++ b/apps/demo-rx-stateful/src/app/all-use-cases/all-use-cases.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AllUseCasesComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(AllUseCasesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/demo-rx-stateful/src/app/all-use-cases/all-use-cases.component.ts b/apps/demo-rx-stateful/src/app/all-use-cases/all-use-cases.component.ts new file mode 100644 index 0000000..aef4caa --- /dev/null +++ b/apps/demo-rx-stateful/src/app/all-use-cases/all-use-cases.component.ts @@ -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() + refreshInterval = 10000 + /** + * Für alle Use Cases eine demo machen + */ + + /** + * Case 1 + * Basic Usage with automatic refetch and a refreshtrigger + */ + case1$ = rxStateful$( + 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() + + 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() + two$ = rxStateful$( + timer(1000).pipe( + switchMap(() => of(null)) + ), + { + refetchStrategies: [withRefetchOnTrigger(this.refresh$)] + } + ).pipe( + collectState() + ) +} + + +function collectState(): OperatorFunction, { + index: number; + value: RxStateful +}[]>{ + return scan, { + index: number; + value: RxStateful + }[]>((acc, value, index) => { + // @ts-ignore + acc.push({ index, value }); + + return acc; + }, []) +} diff --git a/apps/demo-rx-stateful/src/app/all-use-cases/non-flicker/non-flicker.component.ts b/apps/demo-rx-stateful/src/app/all-use-cases/non-flicker/non-flicker.component.ts new file mode 100644 index 0000000..f0826c6 --- /dev/null +++ b/apps/demo-rx-stateful/src/app/all-use-cases/non-flicker/non-flicker.component.ts @@ -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: ` +

DemoRxStatefulComponent

+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+

State Accumulated

+
    +
  • {{ v | json }}
  • +
+
+
+ `, + 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(); + + 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('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(`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(); + } +} diff --git a/apps/demo-rx-stateful/src/app/all-use-cases/rx-stateful-state-visualizer.component.ts b/apps/demo-rx-stateful/src/app/all-use-cases/rx-stateful-state-visualizer.component.ts new file mode 100644 index 0000000..38e3d4a --- /dev/null +++ b/apps/demo-rx-stateful/src/app/all-use-cases/rx-stateful-state-visualizer.component.ts @@ -0,0 +1,24 @@ +import {Component, input} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import {RxStateful} from "@angular-kit/rx-stateful"; + +type Input = { + index: number; + value: RxStateful +} + +@Component({ + selector: 'rx-stateful-state-visualizer', + standalone: true, + imports: [CommonModule], + template: ` +
    + @for(s of state(); track s.index){ +
  • {{s |json}}
  • + } +
+ ` +}) +export class RxStatefulStateVisualizerComponent { + state = input([]) +} diff --git a/apps/demo-rx-stateful/src/app/app.routes.ts b/apps/demo-rx-stateful/src/app/app.routes.ts index 4cea463..5493e88 100644 --- a/apps/demo-rx-stateful/src/app/app.routes.ts +++ b/apps/demo-rx-stateful/src/app/app.routes.ts @@ -1,6 +1,7 @@ import { Route } from '@angular/router'; import {DemoPaginationComponent} from "./demos/demo-pagination.component"; import {DemoBasicUsageComponent} from "./demos/demo-basic-usage.component"; +import {AllUseCasesComponent} from "./all-use-cases/all-use-cases.component"; export const appRoutes: Route[] = [ { @@ -13,5 +14,9 @@ export const appRoutes: Route[] = [ path: 'pagination', component: DemoPaginationComponent, }, - + { + title: 'all-cases', + path: 'all-cases', + component: AllUseCasesComponent, + }, ]; diff --git a/apps/demo/src/app/demos/demo-rx-stateful/demo-rx-stateful.component.ts b/apps/demo/src/app/demos/demo-rx-stateful/demo-rx-stateful.component.ts index ce15c84..ce6e034 100644 --- a/apps/demo/src/app/demos/demo-rx-stateful/demo-rx-stateful.component.ts +++ b/apps/demo/src/app/demos/demo-rx-stateful/demo-rx-stateful.component.ts @@ -88,7 +88,7 @@ export class DemoRxStatefulComponent { // ); - state$ = rxStateful$(this.fetch(4000), { + state$ = rxStateful$(this.fetch(400), { keepValueOnRefresh: false, keepErrorOnRefresh: false, refreshTrigger$: this.refresh$$, diff --git a/libs/rx-stateful/src/lib/rx-stateful$.ts b/libs/rx-stateful/src/lib/rx-stateful$.ts index 285c091..7d7cac5 100644 --- a/libs/rx-stateful/src/lib/rx-stateful$.ts +++ b/libs/rx-stateful/src/lib/rx-stateful$.ts @@ -212,8 +212,8 @@ function createState$( refreshedValue$.pipe( // with this we make sure that we do not turn off the suspsense state as long as a request is running // @ts-ignore - filter(v => !!v.value) - ), + filter(v => v.context !== 'suspense') + ), timer(suspenseThreshold + suspenseTime)] ).pipe(map(() => false)) ) @@ -234,7 +234,7 @@ function createState$( valueFromSourceTrigger$.pipe( // with this we make sure that we do not turn off the suspsense state as long as a request is running // @ts-ignore - filter(v => !!v.value) + filter(v => v.context !== 'suspense') ), timer(suspenseThreshold + suspenseTime) ]).pipe(map(() => false)) @@ -323,12 +323,19 @@ function createState$( */ // @ts-ignore todo refreshTriggerIsBehaivorSubject(mergedConfig) ? skip(1) : pipe(), + // @ts-ignore switchMap(() => sharedSource$.pipe( map(v => mapToValue(v)), deriveInitialValue(mergedConfig) ), ), + share({ + connector: () => new ReplaySubject(1), + resetOnError: true, + resetOnComplete: true, + resetOnRefCountZero: true, + }), ) as Observable>> @@ -347,7 +354,7 @@ function createState$( combineLatest([ refreshedRequest$.pipe( // with this we make sure that we do not turn off the suspsense state as long as a request is running - filter(v => !!v.value) + filter(v => v.context !== 'suspense') ), timer(suspenseThreshold + suspenseTime)] ).pipe(map(() => false))