Skip to content

Commit

Permalink
deprecate old Async function, update error-sample
Browse files Browse the repository at this point in the history
  • Loading branch information
SanderElias committed Jul 21, 2024
1 parent 46ad4b5 commit 04275f1
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 60 deletions.
25 changes: 21 additions & 4 deletions src/app/signal-error/signal-error.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Component, computed, signal } from '@angular/core';
import { map, timer } from 'rxjs';
import { asyncComputed } from 'src/utils/signals/async-computed';

@Component({
Expand All @@ -17,7 +18,16 @@ import { asyncComputed } from 'src/utils/signals/async-computed';
styleUrl: './signal-error.component.css',
})
export default class SignalErrorComponent {
$count = signal(10);
$count = signal(11);

faultyTimer$ = timer(0, 500).pipe(
map(i => {
if (i === 10) {
throw new Error('You left me hanging...');
}
return i;
})
);

even = async n => {
await new Promise(r => setTimeout(r, 250));
Expand All @@ -28,12 +38,19 @@ export default class SignalErrorComponent {
}
};

$result = asyncComputed(async () => {
stupidFn = asyncComputed(() => {
const c = this.$count();
if (c % 3 === 0) {
return this.faultyTimer$; // return an observable that will throw an error in time
}
return this.even(c); // return a promise that will throw an error if the number is not even
});

$result = computed(() => {
try {
return await this.even(c);
return this.stupidFn();
} catch (error: any) {
console.error(error);
// console.error(error);
return `Error: ${error.message}`;
}
});
Expand Down
4 changes: 2 additions & 2 deletions src/app/signal-play/signal-play.service.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { computed, inject, Injectable } from '@angular/core';

import { DemoUserService } from '../demo-users.service';
import { observableComputed } from 'src/utils/signals/observable-computed';
import { asyncComputed } from 'src/utils/signals/async-computed';

@Injectable({
providedIn: 'root',
})
export class SignalPlayService {
users = inject(DemoUserService);

$users = observableComputed(() => this.users.allUsers$);
$users = asyncComputed(() => this.users.allUsers$);

getUser = (n: string) => {
const users = this.$users();
Expand Down
54 changes: 33 additions & 21 deletions src/utils/signals/async-computed.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,81 @@
/* eslint-disable max-len */
import { computed, DestroyRef, effect, inject, type Signal, signal } from '@angular/core';
import { firstValueFrom, isObservable, Observable } from 'rxjs';
import { isPromise } from 'util/types';
import { computed, DestroyRef, effect, inject, isDevMode, type Signal, signal } from '@angular/core';
import { isObservable, Observable, type Subscription } from 'rxjs';
import { isPromise } from './is-promise';

type AsyncComputedFn<T> = () => Promise<T> | Observable<T> | T;
type ObservableComputedFn<T> = () => Observable<T> | Promise<T> | T;
interface AsyncComputed {
/**
* @description Helper to get the result of a promise, or the first emission form a observable into an signal.
* @description Helper to put the outcome(s) of a promise or observable into a signal
* @template T type to use for the output signal, defaults to return type of the AsyncComputedFn
* @param {AsyncComputedFn<T>} AsyncComputedFn function returning a promise or observable. the result is put into the signal when it arrives
* @param {ObservableComputedFn<T>} AsyncComputedFn function returning a promise or observable or a value. the result is put into the signal when it arrives
* @returns {*} {(Signal<T | undefined>)}
*/
<T>(cb: AsyncComputedFn<T>): Signal<T | undefined>;
<T>(cb: ObservableComputedFn<T>): Signal<T | undefined>;
/**
* @description Helper to get the result of a promise, or the first emission form a observable into an signal.
* @description Helper to put the outcome(s) of a promise or observable into a signal
* @template T type to use for the output signal, defaults to return type of the AsyncComputedFn
* @template Y Type to use for the initalValue, default to the type of the initialValue
* @param {AsyncComputedFn<T>} AsyncComputedFn function returning a promise or observable. the result is put into the signal when it arrives
* @param {ObservableComputedFn<T>} AsyncComputedFn function returning a promise or observable. the result is put into the signal when it arrives
* @param {Y} [initialValue] the initial value of the signal.
* @returns {*} {(Signal<T | Y>)}
*/
<X, Y>(cb: AsyncComputedFn<X>, initialValue: Y): Signal<X | Y>;
<X, Y>(cb: ObservableComputedFn<X>, initialValue: Y): Signal<X | Y>;
/**
* @description Helper to get the result of a promise, or the first emission form a observable into an signal.
* @description Helper to put the outcome(s) of a promise or observable into a signal
* @template T type to use for the output signal, defaults to return type of the AsyncComputedFn
* @template Y Type to use for the initalValue, default to the type of the initialValue
* @param {AsyncComputedFn<T>} AsyncComputedFn function returning a promise or observable. the result is put into the signal when it arrives
* @param {ObservableComputedFn<T>} AsyncComputedFn function returning a promise or observable. the result is put into the signal when it arrives
* @param {Y} [initialValue] the initial value of the signal.
* @param {DestroyRef} [destroyRef] a manual provided destroyRef. Mandatory when the function is used outside a injection context
* @returns {*} {(Signal<T | Y>)}
*/
<X, Y>(cb: AsyncComputedFn<X>, initialValue: Y, destroyRef: DestroyRef): Signal<X | Y>;
<X, Y = X>(cb: ObservableComputedFn<X>, initialValue: Y, destroyRef: DestroyRef): Signal<X | Y>;
}

export const asyncComputed: AsyncComputed = <T, Y>(
cb: AsyncComputedFn<T>,
cb: ObservableComputedFn<T>,
initialValue?: Y,
destroyRef = inject(DestroyRef)
): Signal<T | Y | undefined> => {
const state = signal({
value: initialValue,
error: undefined,
// not adding the completed state. a Signals has no way to communicate this
// to its consumers without custom wrapping. That is a different concern that
// is outside the scope of this helper
} as { value?: T | Y | undefined; error?: any });
if (!destroyRef) {
throw new Error('destroyRef is mandatory when used outside a injection context');
}
destroyRef.onDestroy(() => ref.destroy());
destroyRef.onDestroy(() => {
obs?.unsubscribe();
ref.destroy();
});
let obs: Subscription | undefined;
const ref = effect(
async () => {
let value: T | Y | undefined;
try {
obs?.unsubscribe(); // cleanup previous subscription (on new signal emission)
const outcome = cb();
if (isObservable(outcome)) {
value = await firstValueFrom(outcome);
obs = outcome.subscribe({
next: value => state.set({ value, error: undefined }),
error: error => {
state.set({ value: undefined, error });
},
});
} else if (isPromise(outcome)) {
value = await outcome;
const value = await outcome;
state.set({ value, error: undefined });
} else {
value = outcome;
state.set({ value: outcome, error: undefined });
}
state.set({ value, error: undefined });
} catch (e) {
state.set({ value: undefined, error: e });
}
},
{ manualCleanup: true }
{ manualCleanup: true, allowSignalWrites: true }
);

return computed(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,91 +1,78 @@
/* eslint-disable max-len */
import { computed, DestroyRef, effect, inject, type Signal, signal } from '@angular/core';
import { firstValueFrom, isObservable, Observable, type Subscription } from 'rxjs';
import { isPromise } from './is-promise';
import { firstValueFrom, isObservable, Observable } from 'rxjs';
import { isPromise } from 'util/types';

type ObservableComputedFn<T> = () => Observable<T> | Promise<T> | T;
type AsyncComputedFn<T> = () => Promise<T> | Observable<T> | T;
interface AsyncComputed {
/**
* @description Helper to get the result of a promise, or the first emission form a observable into an signal.
* @template T type to use for the output signal, defaults to return type of the AsyncComputedFn
* @param {ObservableComputedFn<T>} AsyncComputedFn function returning a promise or observable. the result is put into the signal when it arrives
* @param {AsyncComputedFn<T>} AsyncComputedFn function returning a promise or observable. the result is put into the signal when it arrives
* @returns {*} {(Signal<T | undefined>)}
*/
<T>(cb: ObservableComputedFn<T>): Signal<T | undefined>;
<T>(cb: AsyncComputedFn<T>): Signal<T | undefined>;
/**
* @description Helper to get the result of a promise, or the first emission form a observable into an signal.
* @template T type to use for the output signal, defaults to return type of the AsyncComputedFn
* @template Y Type to use for the initalValue, default to the type of the initialValue
* @param {ObservableComputedFn<T>} AsyncComputedFn function returning a promise or observable. the result is put into the signal when it arrives
* @param {AsyncComputedFn<T>} AsyncComputedFn function returning a promise or observable. the result is put into the signal when it arrives
* @param {Y} [initialValue] the initial value of the signal.
* @returns {*} {(Signal<T | Y>)}
*/
<X, Y>(cb: ObservableComputedFn<X>, initialValue: Y): Signal<X | Y>;
<X, Y>(cb: AsyncComputedFn<X>, initialValue: Y): Signal<X | Y>;
/**
* @description Helper to get the result of a promise, or the first emission form a observable into an signal.
* @template T type to use for the output signal, defaults to return type of the AsyncComputedFn
* @template Y Type to use for the initalValue, default to the type of the initialValue
* @param {ObservableComputedFn<T>} AsyncComputedFn function returning a promise or observable. the result is put into the signal when it arrives
* @param {AsyncComputedFn<T>} AsyncComputedFn function returning a promise or observable. the result is put into the signal when it arrives
* @param {Y} [initialValue] the initial value of the signal.
* @param {DestroyRef} [destroyRef] a manual provided destroyRef. Mandatory when the function is used outside a injection context
* @returns {*} {(Signal<T | Y>)}
*/
<X, Y>(cb: ObservableComputedFn<X>, initialValue: Y, destroyRef: DestroyRef): Signal<X | Y>;
<X, Y>(cb: AsyncComputedFn<X>, initialValue: Y, destroyRef: DestroyRef): Signal<X | Y>;
}

export const observableComputed: AsyncComputed = <T, Y>(
cb: ObservableComputedFn<T>,
export const oldAsyncComputed: AsyncComputed = <T, Y>(
cb: AsyncComputedFn<T>,
initialValue?: Y,
destroyRef = inject(DestroyRef)
): Signal<T | Y | undefined> => {
const state = signal({
value: initialValue,
error: undefined,
// not adding the completed state. a Signals has no way to communicate this
// to its consumers without custom wrapping. That is a different concern that
// is outside the scope of this helper
} as { value?: T | Y | undefined; error?: any });
if (!destroyRef) {
throw new Error('destroyRef is mandatory when used outside a injection context');
}
destroyRef.onDestroy(() => {
obs?.unsubscribe();
ref.destroy();
});
let obs: Subscription | undefined;
destroyRef.onDestroy(() => ref.destroy());
const ref = effect(
async () => {
let value: T | Y | undefined;
try {
obs?.unsubscribe(); // cleanup previous subscription (on new signal emission)
const outcome = cb();
if (isObservable(outcome)) {
obs = outcome.subscribe({
next: value => state.set({ value, error: undefined }),
error: error => {
state.set({ value: undefined, error });
},
});
value = await firstValueFrom(outcome);
} else if (isPromise(outcome)) {
const value = await outcome;
state.set({ value, error: undefined });
value = await outcome;
} else {
state.set({ value: outcome, error: undefined });
value = outcome;
}
state.set({ value, error: undefined });
} catch (e) {
// this is needed because there might be an error in the CD that is not inside the observable stream
state.set({ value: undefined, error: e });
}
},
{ manualCleanup: true, allowSignalWrites: true }
{ manualCleanup: true }
);

return computed(() => {
const currentState = state();
if (currentState.error) {
// rethrow error to be handled by the user
throw currentState.error;
// note to self: do not wrap this in a new error, as it will hide the original stack trace
}
return currentState.value;
});
};


0 comments on commit 04275f1

Please sign in to comment.