Thanks to visit codestin.com
Credit goes to github.com

Skip to content

added logging to the hold method #678

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion libs/state/spec/rx-state.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ErrorHandler, Injector } from '@angular/core';
// tslint:disable-next-line:nx-enforce-module-boundaries
import { jestMatcher } from '@test-helpers';
import { fakeAsync, TestBed } from '@angular/core/testing';
Expand All @@ -10,7 +11,10 @@ import { TestScheduler } from 'rxjs/testing';
// tslint:disable-next-line:nx-enforce-module-boundaries
import { RxState, select } from '@rx-angular/state';
import { map, pluck, switchMap, take, takeUntil } from 'rxjs/operators';
import { asyncScheduler, from, interval, of, scheduled, Subject } from 'rxjs';
import { from, interval, of, Subject, throwError, asyncScheduler, from, interval, scheduled, Subject } from 'rxjs';
import {
ɵsetCurrentInjector as setCurrentInjector
} from '@angular/core';

function setupState<T extends object>(cfg: { initialState?: T }) {
const { initialState } = { ...cfg };
Expand Down Expand Up @@ -587,5 +591,18 @@ describe('RxStateService', () => {
state.hold(of(1, 2, 3), effect);
expect(calls).toBe(3);
}));

it('should invoke provided ErrorHandler', fakeAsync(() => {
const state = setupState({ initialState: initialPrimitiveState });
const customErrorHandler: ErrorHandler = {
handleError: jest.fn()
};
TestBed.overrideProvider(ErrorHandler, { useValue: customErrorHandler });
setCurrentInjector(TestBed.inject(Injector));
state.hold(throwError(new Error('something went wrong')));
expect(customErrorHandler.handleError).toHaveBeenCalledWith(
new Error('something went wrong')
);
}));
});
});
95 changes: 71 additions & 24 deletions libs/state/src/lib/rx-state.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
import { Injectable, OnDestroy } from '@angular/core';
import { EMPTY, isObservable, Observable, OperatorFunction, Subscribable, Subscription, Unsubscribable } from 'rxjs';
import {
ErrorHandler,
inject,
Injectable,
InjectFlags,
OnDestroy,
} from '@angular/core';
import {
EMPTY,
isObservable,
Observable,
OperatorFunction,
Subscribable,
Subscription,
Unsubscribable,
} from 'rxjs';
import { catchError, map, pluck, tap } from 'rxjs/operators';
import { isKeyOf, isOperateFnArrayGuard, isStringArrayGuard, pipeFromArray, safePluck } from './core';
import { AccumulationFn, createAccumulationObservable, createSideEffectObservable } from './cdk';
import {
isKeyOf,
isOperateFnArrayGuard,
isStringArrayGuard,
pipeFromArray,
safePluck,
} from './core';
import {
AccumulationFn,
createAccumulationObservable,
createSideEffectObservable,
} from './cdk';
import { stateful } from './rxjs/operators';

type ProjectStateFn<T> = (oldState: T) => Partial<T>;
Expand Down Expand Up @@ -120,30 +144,38 @@ export class RxState<T extends object> implements OnDestroy, Subscribable<T> {
k3: K3
): T[K1][K2][K3];
/** @internal **/
get<K1 extends keyof T,
get<
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2],
K4 extends keyof T[K1][K2][K3]>(k1: K1, k2: K2, k3: K3, k4: K4): T[K1][K2][K3][K4];
K4 extends keyof T[K1][K2][K3]
>(k1: K1, k2: K2, k3: K3, k4: K4): T[K1][K2][K3][K4];
/** @internal **/
get<K1 extends keyof T,
get<
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2],
K4 extends keyof T[K1][K2][K3],
K5 extends keyof T[K1][K2][K3][K4]>(k1: K1, k2: K2, k3: K3, k4: K4, k5: K5): T[K1][K2][K3][K4][K5];
K5 extends keyof T[K1][K2][K3][K4]
>(k1: K1, k2: K2, k3: K3, k4: K4, k5: K5): T[K1][K2][K3][K4][K5];
/** @internal **/
get<K1 extends keyof T,
get<
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2],
K4 extends keyof T[K1][K2][K3],
K5 extends keyof T[K1][K2][K3][K4],
K6 extends keyof T[K1][K2][K3][K4][K5]>(k1: K1, k2: K2, k3: K3, k4: K4, k5: K5, k6: K6): T[K1][K2][K3][K4][K5][K6];
K6 extends keyof T[K1][K2][K3][K4][K5]
>(k1: K1, k2: K2, k3: K3, k4: K4, k5: K5, k6: K6): T[K1][K2][K3][K4][K5][K6];
/** @internal **/
get<K1 extends keyof T,
get<
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2],
K4 extends keyof T[K1][K2][K3],
K5 extends keyof T[K1][K2][K3][K4],
K6 extends keyof T[K1][K2][K3][K4][K5]>(
K6 extends keyof T[K1][K2][K3][K4][K5]
>(
...keys:
| [K1]
| [K1, K2]
Expand All @@ -163,9 +195,9 @@ export class RxState<T extends object> implements OnDestroy, Subscribable<T> {
if (!!keys && keys.length) {
return safePluck(this.accumulator.state, keys);
} else {
return hasStateAnyKeys ?
this.accumulator.state :
undefined as unknown as T;
return hasStateAnyKeys
? this.accumulator.state
: ((undefined as unknown) as T);
}
}

Expand Down Expand Up @@ -472,33 +504,41 @@ export class RxState<T extends object> implements OnDestroy, Subscribable<T> {
/**
* @internal
*/
select<K1 extends keyof T,
select<
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2]>(k1: K1, k2: K2, k3: K3): Observable<T[K1][K2][K3]>;
K3 extends keyof T[K1][K2]
>(k1: K1, k2: K2, k3: K3): Observable<T[K1][K2][K3]>;
/**
* @internal
*/
select<K1 extends keyof T,
select<
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2],
K4 extends keyof T[K1][K2][K3]>(k1: K1, k2: K2, k3: K3, k4: K4): Observable<T[K1][K2][K3][K4]>;
K4 extends keyof T[K1][K2][K3]
>(k1: K1, k2: K2, k3: K3, k4: K4): Observable<T[K1][K2][K3][K4]>;
/**
* @internal
*/
select<K1 extends keyof T,
select<
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2],
K4 extends keyof T[K1][K2][K3],
K5 extends keyof T[K1][K2][K3][K4]>(k1: K1, k2: K2, k3: K3, k4: K4, k5: K5): Observable<T[K1][K2][K3][K4][K5]>;
K5 extends keyof T[K1][K2][K3][K4]
>(k1: K1, k2: K2, k3: K3, k4: K4, k5: K5): Observable<T[K1][K2][K3][K4][K5]>;
/**
* @internal
*/
select<K1 extends keyof T,
select<
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2],
K4 extends keyof T[K1][K2][K3],
K5 extends keyof T[K1][K2][K3][K4],
K6 extends keyof T[K1][K2][K3][K4][K5]>(
K6 extends keyof T[K1][K2][K3][K4][K5]
>(
k1: K1,
k2: K2,
k3: K3,
Expand Down Expand Up @@ -547,7 +587,14 @@ export class RxState<T extends object> implements OnDestroy, Subscribable<T> {
obsOrObsWithSideEffect: Observable<S>,
sideEffectFn?: (arg: S) => void
): void {
const sideEffect = obsOrObsWithSideEffect.pipe(catchError(e => EMPTY))
const sideEffect = obsOrObsWithSideEffect.pipe(
catchError((e) => {
// used injector for compatibility with https://github.com/rx-angular/rx-angular/blob/master/libs/state/docs/usage.md#inherit
const errorHandler = inject(ErrorHandler, InjectFlags.Optional);
errorHandler?.handleError(e);
Comment on lines +593 to +594
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last detail, why considering the ErrorHandler as optional? It should always be defined.

Suggested change
const errorHandler = inject(ErrorHandler, InjectFlags.Optional);
errorHandler?.handleError(e);
const errorHandler = inject(ErrorHandler);
errorHandler.handleError(e);

Copy link
Member

@arturovt arturovt Aug 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

He has probably added it because it throws inject() must be called from an injection context since there's no injector on the instruction stack.

Your recommendation with this one is correct:

constructor(private errorHandler: ErrorHandler) {}

Since the stable version is not released yet I think it's ok to have breaking changes. We can have a migration script for that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can create a migration script, but honestly, this looks bad:

  constructor(private errorHandler: ErrorHandler) {
    super(this.errorHandler);
  }

I would prefer keeping the InjectFlags.Optional hack to hide this noise from the user.

Copy link
Member

@arturovt arturovt Aug 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just checked that code and it seems like it's working only inside tests for now because the injector is explicitly set there. E.g. this doesn't work even with inject flags:

@Component({
  selector: 'app-root',
  template: '<button (click)="onClick()">Click me</button>',
})
export class AppComponent {
  onClick(): void {
    console.log(inject(ErrorHandler, InjectFlags.Optional));
  }
}

That would respect inject flags if there's any injector on the stack:

function injectInjectorOnly(token, flags = InjectFlags.Default) {
    if (_currentInjector === undefined) {
        throw new Error(`inject() must be called from an injection context`);
    }
}
function ɵɵinject(token, flags = InjectFlags.Default) {
    // The `getInjectImplementation` returns `undefined` here
    return (getInjectImplementation() || injectInjectorOnly)(resolveForwardRef(token), flags);
}

I'm wondering if it can be moved to the constructor but it also has to be wrapped into try-catch to prevent the error if there is no current injector:

export class ... {
  private errorHandler: ErrorHandler | null;

  constructor() {
    try {
      this.errorHandler = inject(ErrorHandler);
    } catch {
      this.errorHandler = null;
    }
  }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if it can be moved to the constructor but it also has to be wrapped into try-catch to prevent the error if there is no current injector:

export class ... {
  private errorHandler: ErrorHandler | null;

  constructor() {
    try {
      this.errorHandler = inject(ErrorHandler);
    } catch {
      this.errorHandler = null;
    }
  }

this sounds like the only valid solution for me since the approach with extending from RxState is pretty common.

Copy link
Author

@d-stolyar d-stolyar Aug 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about this? @hoebbelsB

class ... {
  private errorHandler: ErrorHandler

  constructor() {
    try {
      this.errorHandler = inject(ErrorHandler)
    } catch {
      const injector = Injector.create({
        providers: [{ provide: ErrorHandler, deps: [] }]
      });
      this.errorHandler = injector.get(ErrorHandler);
    }
  }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will create ErrorHandlers per each RxState instance and will have nothing to do with the global error handler (the one provided by a user).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To conclude, we should try this approach and decide:

export class ... {
  private errorHandler: ErrorHandler | null;

  constructor() {
    try {
      this.errorHandler = inject(ErrorHandler);
    } catch {
      this.errorHandler = null;
    }
  }

return EMPTY;
})
);
if (typeof sideEffectFn === 'function') {
this.effectObservable.nextEffectObservable(
sideEffect.pipe(tap(sideEffectFn))
Expand Down