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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ 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))