Skip to content
Draft
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
16 changes: 14 additions & 2 deletions apps/demo/src/app/counter-rx-mutation/counter-rx-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from '@angular-architects/ngrx-toolkit';
import { CommonModule } from '@angular/common';
import { Component, computed, signal } from '@angular/core';
import { delay, Observable, of, throwError } from 'rxjs';
import { catchError, delay, Observable, of, throwError } from 'rxjs';

export type Params = {
value: number;
Expand Down Expand Up @@ -51,6 +51,12 @@ export class CounterRxMutation {
onSuccess: (response) => {
console.log('Counter sent to server:', response);
},
// TODO - In the current state, parsing for the error does not narrow unless either
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Provided this is the behavior we think makes sense, then I just need to add a test at this point. Thoughts?

// - 3rd error type generic specified
// - No generic types specified
// This is inline with the existing configuration of this `httpMutation` example before this proposed change
// If you uncomment the following, you must still specify the error type or drop the explicit types
// parseError: (error) => error as string,
onError: (error) => {
console.error('Failed to send counter:', error);
},
Expand Down Expand Up @@ -108,5 +114,11 @@ function calcSum(a: number, b: number): Observable<number> {
result,
}));
}
return of(result).pipe(delay(500));
return of(result).pipe(
delay(500),
catchError((error) => {
console.error('Error in calcSum:', error);
return of(1);
}),
);
}
20 changes: 13 additions & 7 deletions libs/ngrx-toolkit/src/lib/mutation/http-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,20 @@ export type HttpMutationRequest = {
| boolean;
};

export type HttpMutationOptions<Parameter, Result> = Omit<
RxMutationOptions<Parameter, NoInfer<Result>>,
export type HttpMutationOptions<Parameter, Result, Err = unknown> = Omit<
RxMutationOptions<Parameter, NoInfer<Result>, NoInfer<Err>>,
'operation'
> & {
request: (param: Parameter) => HttpMutationRequest;
parse?: (response: unknown) => Result;
parseError?: (error: unknown) => Err;
};

export type HttpMutation<Parameter, Result> = Mutation<Parameter, Result> & {
export type HttpMutation<Parameter, Result, Err = unknown> = Mutation<
Parameter,
Result,
Err
> & {
uploadProgress: Signal<HttpProgressEvent | undefined>;
downloadProgress: Signal<HttpProgressEvent | undefined>;
headers: Signal<HttpHeaders | undefined>;
Expand Down Expand Up @@ -108,11 +113,11 @@ export type HttpMutation<Parameter, Result> = Mutation<Parameter, Result> & {
* @param options The options for the HTTP mutation.
* @returns The HTTP mutation.
*/
export function httpMutation<Parameter, Result>(
export function httpMutation<Parameter, Result, Err = unknown>(
optionsOrRequest:
| HttpMutationOptions<Parameter, Result>
| HttpMutationOptions<Parameter, Result, Err>
| ((param: Parameter) => HttpMutationRequest),
): HttpMutation<Parameter, Result> {
): HttpMutation<Parameter, Result, Err> {
const httpClient = inject(HttpClient);

const options =
Expand All @@ -121,6 +126,7 @@ export function httpMutation<Parameter, Result>(
: optionsOrRequest;

const parse = options.parse ?? ((raw: unknown) => raw as Result);
const parseError = options.parseError ?? ((raw: unknown) => raw as Err);

const uploadProgress = signal<HttpProgressEvent | undefined>(undefined);
const downloadProgress = signal<HttpProgressEvent | undefined>(undefined);
Expand Down Expand Up @@ -161,7 +167,7 @@ export function httpMutation<Parameter, Result>(
);
});
},
}) as HttpMutation<Parameter, Result>;
}) as HttpMutation<Parameter, Result, Err>;

mutation.uploadProgress = uploadProgress;
mutation.downloadProgress = downloadProgress;
Expand Down
12 changes: 6 additions & 6 deletions libs/ngrx-toolkit/src/lib/mutation/mutation.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { Signal } from '@angular/core';

export type MutationResult<Result> =
export type MutationResult<Result, Err> =
| {
status: 'success';
value: Result;
}
| {
status: 'error';
error: unknown;
error: Err;
}
| {
status: 'aborted';
};

export type MutationStatus = 'idle' | 'pending' | 'error' | 'success';

export type Mutation<Parameter, Result> = {
(params: Parameter): Promise<MutationResult<Result>>;
export type Mutation<Parameter, Result, Err> = {
(params: Parameter): Promise<MutationResult<Result, Err>>;
status: Signal<MutationStatus>;
value: Signal<Result | undefined>;
isPending: Signal<boolean>;
isSuccess: Signal<boolean>;
error: Signal<unknown>;
hasValue(): this is Mutation<Exclude<Parameter, undefined>, Result>;
error: Signal<Err | undefined>;
hasValue(): this is Mutation<Exclude<Parameter, undefined>, Result, Err>;
};
34 changes: 18 additions & 16 deletions libs/ngrx-toolkit/src/lib/mutation/rx-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import {
import { concatOp, FlatteningOperator } from '../flattening-operator';
import { Mutation, MutationResult, MutationStatus } from './mutation';

export type Operation<Parameter, Result> = (param: Parameter) => Result;
export type Operation<Parameter, Result, Err = unknown> = (
param: Parameter,
) => Result;

export interface RxMutationOptions<Parameter, Result> {
operation: Operation<Parameter, Observable<Result>>;
export interface RxMutationOptions<Parameter, Result, Err = unknown> {
operation: Operation<Parameter, Observable<Result>, Observable<Err>>;
onSuccess?: (result: Result, param: Parameter) => void;
onError?: (error: unknown, param: Parameter) => void;
onError?: (error: Err, param: Parameter) => void;
operator?: FlatteningOperator;
injector?: Injector;
}
Expand Down Expand Up @@ -86,14 +88,14 @@ export interface RxMutationOptions<Parameter, Result> {
* @param options
* @returns the actual mutation function along tracking data as properties/methods
*/
export function rxMutation<Parameter, Result>(
export function rxMutation<Parameter, Result, Err = unknown>(
optionsOrOperation:
| RxMutationOptions<Parameter, Result>
| Operation<Parameter, Observable<Result>>,
): Mutation<Parameter, Result> {
| RxMutationOptions<Parameter, Result, Err>
| Operation<Parameter, Observable<Result>, Observable<Err>>,
): Mutation<Parameter, Result, Err> {
const inputSubject = new Subject<{
param: Parameter;
resolve: (result: MutationResult<Result>) => void;
resolve: (result: MutationResult<Result, Err>) => void;
}>();

const options =
Expand All @@ -106,15 +108,15 @@ export function rxMutation<Parameter, Result>(
const destroyRef = options.injector?.get(DestroyRef) ?? inject(DestroyRef);

const callCount = signal(0);
const errorSignal = signal<unknown>(undefined);
const errorSignal = signal<Err | undefined>(undefined);
const idle = signal(true);
const isPending = computed(() => callCount() > 0);
const value = signal<Result | undefined>(undefined);
const isSuccess = computed(() => !idle() && !isPending() && !errorSignal());

const hasValue = function (
this: Mutation<Parameter, Result>,
): this is Mutation<Exclude<Parameter, undefined>, Result> {
this: Mutation<Parameter, Result, Err>,
): this is Mutation<Exclude<Parameter, undefined>, Result, Err> {
return typeof value() !== 'undefined';
};

Expand Down Expand Up @@ -147,7 +149,7 @@ export function rxMutation<Parameter, Result>(
errorSignal.set(undefined);
value.set(result);
}),
catchError((error: unknown) => {
catchError((error: Err) => {
options.onError?.(error, input.param);
errorSignal.set(error);
value.set(undefined);
Expand All @@ -165,7 +167,7 @@ export function rxMutation<Parameter, Result>(
} else if (innerStatus === 'error') {
input.resolve({
status: 'error',
error: errorSignal(),
error: errorSignal() as Err,
});
} else {
input.resolve({
Expand All @@ -183,7 +185,7 @@ export function rxMutation<Parameter, Result>(
.subscribe();

const mutationFn = (param: Parameter) => {
return new Promise<MutationResult<Result>>((resolve) => {
return new Promise<MutationResult<Result, Err>>((resolve) => {
if (callCount() > 0 && flatteningOp.exhaustSemantics) {
resolve({
status: 'aborted',
Expand All @@ -197,7 +199,7 @@ export function rxMutation<Parameter, Result>(
});
};

const mutation = mutationFn as Mutation<Parameter, Result>;
const mutation = mutationFn as Mutation<Parameter, Result, Err>;
mutation.status = status;
mutation.isPending = isPending;
mutation.error = errorSignal;
Expand Down
7 changes: 4 additions & 3 deletions libs/ngrx-toolkit/src/lib/with-mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Mutation, MutationStatus } from './mutation/mutation';

// NamedMutationMethods below will infer the actual parameter and return types
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type MutationsDictionary = Record<string, Mutation<any, any>>;
type MutationsDictionary = Record<string, Mutation<any, any, any>>;

// withMethods uses Record<string, Function> internally
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
Expand All @@ -31,9 +31,10 @@ type NamedMutationProps<T extends MutationsDictionary> = {
type NamedMutationMethods<T extends MutationsDictionary> = {
[Prop in keyof T as `${Prop & string}`]: T[Prop] extends Mutation<
infer P,
infer R
infer R,
infer E
>
? Mutation<P, R>
? Mutation<P, R, E>
: never;
};

Expand Down
Loading