mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-26 23:02:57 +08:00
207 lines
4.9 KiB
TypeScript
207 lines
4.9 KiB
TypeScript
import { UserFriendlyError } from '@affine/error';
|
|
import {
|
|
catchError,
|
|
connect,
|
|
distinctUntilChanged,
|
|
EMPTY,
|
|
exhaustMap,
|
|
merge,
|
|
mergeMap,
|
|
Observable,
|
|
type ObservableInput,
|
|
type ObservedValueOf,
|
|
of,
|
|
type OperatorFunction,
|
|
pipe,
|
|
retry,
|
|
switchMap,
|
|
throwError,
|
|
timer,
|
|
} from 'rxjs';
|
|
|
|
import { MANUALLY_STOP } from '../utils';
|
|
import type { LiveData } from './livedata';
|
|
|
|
/**
|
|
* An operator that maps the value to the `LiveData`.
|
|
*/
|
|
export function mapInto<T>(l$: LiveData<T>) {
|
|
return pipe(
|
|
mergeMap((value: T) => {
|
|
l$.next(value);
|
|
return EMPTY;
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* An operator that catches the error and sends it to the `LiveData`.
|
|
*
|
|
* The `LiveData` will be set to `null` when the observable completes. This is useful for error state recovery.
|
|
*
|
|
* @param cb A callback that will be called when an error occurs.
|
|
*/
|
|
export function catchErrorInto<Error = any>(
|
|
l$: LiveData<Error | null>,
|
|
cb?: (error: Error) => void
|
|
) {
|
|
return pipe(
|
|
onComplete(() => l$.next(null)),
|
|
catchError((error: any) => {
|
|
l$.next(error);
|
|
cb?.(error);
|
|
return EMPTY;
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* An operator that calls the callback when the observable starts.
|
|
*/
|
|
export function onStart<T>(cb: () => void): OperatorFunction<T, T> {
|
|
return observable$ =>
|
|
new Observable(subscribe => {
|
|
cb();
|
|
return observable$.subscribe(subscribe);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* An operator that calls the callback when the observable completes.
|
|
*/
|
|
export function onComplete<T>(cb: () => void): OperatorFunction<T, T> {
|
|
return observable$ =>
|
|
new Observable(subscribe => {
|
|
return observable$.subscribe({
|
|
complete() {
|
|
cb();
|
|
subscribe.complete();
|
|
},
|
|
error(err) {
|
|
subscribe.error(err);
|
|
},
|
|
next(value) {
|
|
subscribe.next(value);
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Convert a promise to an observable.
|
|
*
|
|
* like `from` but support `AbortSignal`.
|
|
*/
|
|
export function fromPromise<T>(
|
|
promise: Promise<T> | ((signal: AbortSignal) => Promise<T>)
|
|
): Observable<T> {
|
|
return new Observable(subscriber => {
|
|
const abortController = new AbortController();
|
|
|
|
const rawPromise =
|
|
promise instanceof Function ? promise(abortController.signal) : promise;
|
|
|
|
rawPromise
|
|
.then(value => {
|
|
subscriber.next(value);
|
|
subscriber.complete();
|
|
})
|
|
.catch(error => {
|
|
subscriber.error(error);
|
|
});
|
|
|
|
return () => abortController.abort(MANUALLY_STOP);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* An operator that retries the source observable when an error occurs.
|
|
*
|
|
* https://en.wikipedia.org/wiki/Exponential_backoff
|
|
*/
|
|
export function backoffRetry<T>({
|
|
when,
|
|
count = 3,
|
|
delay = 200,
|
|
maxDelay = 15000,
|
|
}: {
|
|
when?: (err: any) => boolean;
|
|
count?: number;
|
|
delay?: number;
|
|
maxDelay?: number;
|
|
} = {}) {
|
|
return (obs$: Observable<T>) =>
|
|
obs$.pipe(
|
|
retry({
|
|
count,
|
|
delay: (err, retryIndex) => {
|
|
if (when && !when(err)) {
|
|
return throwError(() => err);
|
|
}
|
|
const d = Math.pow(2, retryIndex - 1) * delay;
|
|
return timer(Math.min(d, maxDelay));
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
export function smartRetry<T>({
|
|
count = 3,
|
|
delay = 200,
|
|
maxDelay = 15000,
|
|
}: {
|
|
count?: number;
|
|
delay?: number;
|
|
maxDelay?: number;
|
|
} = {}) {
|
|
return (obs$: Observable<T>) =>
|
|
obs$.pipe(
|
|
backoffRetry({
|
|
when: UserFriendlyError.isNetworkError,
|
|
count: Infinity,
|
|
delay,
|
|
maxDelay,
|
|
}),
|
|
backoffRetry({
|
|
when: UserFriendlyError.notNetworkError,
|
|
count,
|
|
delay,
|
|
maxDelay,
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* An operator that combines `exhaustMap` and `switchMap`.
|
|
*
|
|
* This operator executes the `comparator` on each input, acting as an `exhaustMap` when the `comparator` returns `true`
|
|
* and acting as a `switchMap` when the comparator returns `false`.
|
|
*
|
|
* It is more useful for async processes that are relatively stable in results but sensitive to input.
|
|
* For example, when requesting the user's subscription status, `exhaustMap` is used because the user's subscription
|
|
* does not change often, but when switching users, the request should be made immediately like `switchMap`.
|
|
*
|
|
* @param onSwitch callback will be executed when `switchMap` occurs (including the first execution).
|
|
*/
|
|
export function exhaustMapSwitchUntilChanged<T, O extends ObservableInput<any>>(
|
|
comparator: (previous: T, current: T) => boolean,
|
|
project: (value: T, index: number) => O,
|
|
onSwitch?: (value: T) => void
|
|
): OperatorFunction<T, ObservedValueOf<O>> {
|
|
return pipe(
|
|
connect(shared$ =>
|
|
shared$.pipe(
|
|
distinctUntilChanged(comparator),
|
|
switchMap(value => {
|
|
onSwitch?.(value);
|
|
return merge(of(value), shared$).pipe(
|
|
exhaustMap((value, index) => {
|
|
return project(value, index);
|
|
})
|
|
);
|
|
})
|
|
)
|
|
)
|
|
);
|
|
}
|