From 8ea930d4f240f5e052aecd94e42d3728e3c3cd40 Mon Sep 17 00:00:00 2001 From: Josep M Sobrepere Date: Sat, 10 Oct 2020 04:40:20 +0200 Subject: [PATCH] perf(core): lots of performance improvements --- .../core/src/bind/connectObservable.test.tsx | 21 +++-- .../core/src/internal/BehaviorObservable.ts | 6 -- packages/core/src/internal/react-enhancer.ts | 94 +++++++------------ packages/core/src/internal/useObservable.ts | 63 +++++-------- 4 files changed, 74 insertions(+), 110 deletions(-) diff --git a/packages/core/src/bind/connectObservable.test.tsx b/packages/core/src/bind/connectObservable.test.tsx index 8174c7f..7b52dad 100644 --- a/packages/core/src/bind/connectObservable.test.tsx +++ b/packages/core/src/bind/connectObservable.test.tsx @@ -14,6 +14,7 @@ import { Subject, throwError, Observable, + merge, } from "rxjs" import { delay, @@ -385,11 +386,17 @@ describe("connectObservable", () => { }) it("allows to retry the errored observable after a grace period of time", async () => { - let errStream = new Subject() + const errStream = new Subject() + const nextStream = new Subject() const [useError, error$] = bind( - defer(() => { - return (errStream = new Subject()) - }), + merge( + errStream.pipe( + map((x) => { + throw x + }), + ), + nextStream, + ), ) const ErrorComponent = () => { @@ -398,7 +405,7 @@ describe("connectObservable", () => { } const errorCallback = jest.fn() - error$.pipe(catchError(() => [])).subscribe() + error$.pipe(catchError((_, caught) => caught)).subscribe() const { unmount } = render( Loading...}> @@ -411,7 +418,7 @@ describe("connectObservable", () => { expect(screen.queryByText("ALL GOOD")).toBeNull() await componentAct(async () => { - errStream.error("controlled error") + errStream.next("controlled error") await wait(50) }) @@ -439,7 +446,7 @@ describe("connectObservable", () => { expect(screen.queryByText("Loading...")).not.toBeNull() await componentAct(async () => { - errStream.next("ALL GOOD") + nextStream.next("ALL GOOD") await wait(50) }) diff --git a/packages/core/src/internal/BehaviorObservable.ts b/packages/core/src/internal/BehaviorObservable.ts index fa14757..f62a1c8 100644 --- a/packages/core/src/internal/BehaviorObservable.ts +++ b/packages/core/src/internal/BehaviorObservable.ts @@ -3,9 +3,3 @@ import { Observable } from "rxjs" export interface BehaviorObservable extends Observable { getValue: () => any } - -export const enum Action { - Error, - Value, - Suspense, -} diff --git a/packages/core/src/internal/react-enhancer.ts b/packages/core/src/internal/react-enhancer.ts index 44e761f..658e236 100644 --- a/packages/core/src/internal/react-enhancer.ts +++ b/packages/core/src/internal/react-enhancer.ts @@ -1,81 +1,59 @@ import { Observable } from "rxjs" import { SUSPENSE } from "../SUSPENSE" -import { BehaviorObservable, Action } from "./BehaviorObservable" +import { BehaviorObservable } from "./BehaviorObservable" import { EMPTY_VALUE } from "./empty-value" const reactEnhancer = (source$: Observable): BehaviorObservable => { - const result = new Observable((subscriber) => { - let latestValue = EMPTY_VALUE - return source$.subscribe( - (value) => { - if (!Object.is(latestValue, value)) { - subscriber.next((latestValue = value)) - } - }, - (e) => { - subscriber.error(e) - }, - ) - }) as BehaviorObservable + const result = new Observable((subscriber) => + source$.subscribe(subscriber), + ) as BehaviorObservable - let promise: undefined | { type: Action.Suspense; payload: Promise } - let error: - | typeof EMPTY_VALUE - | { type: Action.Error; payload: any } = EMPTY_VALUE - const getValue = (): { type: Action; payload: any } => { + let promise: Promise | undefined + let error: any = EMPTY_VALUE + const getValue = (): T => { let timeoutToken if (error !== EMPTY_VALUE) { clearTimeout(timeoutToken) timeoutToken = setTimeout(() => { error = EMPTY_VALUE }, 50) - return error + throw error } try { - return { - type: Action.Value, - payload: (source$ as BehaviorObservable).getValue(), - } + return (source$ as BehaviorObservable).getValue() } catch (e) { - if (promise) return promise + if (promise) throw promise - let value: - | typeof EMPTY_VALUE - | { type: Action.Value; payload: T } = EMPTY_VALUE + let value: typeof EMPTY_VALUE | T = EMPTY_VALUE - promise = { - type: Action.Suspense, - payload: new Promise((res) => { - const subscription = result.subscribe( - (v) => { - if (v !== (SUSPENSE as any)) { - value = { type: Action.Value, payload: v } - subscription && subscription.unsubscribe() - res(v) - } - }, - (e) => { - error = { type: Action.Error, payload: e } - timeoutToken = setTimeout(() => { - error = EMPTY_VALUE - }, 50) - res() - }, - ) - if (value !== EMPTY_VALUE) { - subscription.unsubscribe() - } - }).finally(() => { - promise = undefined - }), - } + promise = new Promise((res) => { + const subscription = result.subscribe( + (v) => { + if (v !== (SUSPENSE as any)) { + value = v + subscription && subscription.unsubscribe() + res(v) + } + }, + (e) => { + error = e + timeoutToken = setTimeout(() => { + error = EMPTY_VALUE + }, 50) + res() + }, + ) + if (value !== EMPTY_VALUE) { + subscription.unsubscribe() + } + }).finally(() => { + promise = undefined + }) - if (value !== EMPTY_VALUE) { - return value - } + if (value !== EMPTY_VALUE) return value - return error !== EMPTY_VALUE ? error : promise + throw error !== EMPTY_VALUE ? error : promise } } result.getValue = getValue diff --git a/packages/core/src/internal/useObservable.ts b/packages/core/src/internal/useObservable.ts index c94042a..b9dcaae 100644 --- a/packages/core/src/internal/useObservable.ts +++ b/packages/core/src/internal/useObservable.ts @@ -1,48 +1,35 @@ -import { useEffect, useReducer } from "react" -import { BehaviorObservable, Action } from "./BehaviorObservable" +import { useEffect, useState } from "react" +import { BehaviorObservable } from "./BehaviorObservable" import { SUSPENSE } from "../SUSPENSE" import { EMPTY_VALUE } from "./empty-value" -const reducer = ( - current: { type: Action; payload: any }, - action: { type: Action; payload: any }, -) => - current.type === action.type && Object.is(current.payload, action.payload) - ? current - : action - -const init = (source$: BehaviorObservable) => source$.getValue() - export const useObservable = ( source$: BehaviorObservable, ): Exclude => { - const [state, dispatch] = useReducer(reducer, source$, init) + const [state, setState] = useState(source$.getValue) useEffect(() => { - const onNext = (value: O | typeof SUSPENSE) => { - if ((value as any) === SUSPENSE) { - dispatch(source$.getValue()) - } else { - dispatch({ - type: Action.Value, - payload: value, - }) - } - } - const onError = (error: any) => - dispatch({ - type: Action.Error, - payload: error, - }) - - let val: O | typeof SUSPENSE = SUSPENSE + let prevVal: O | typeof SUSPENSE = EMPTY_VALUE let err: any = EMPTY_VALUE - let subscription = source$.subscribe( - (v) => (val = v), - (e) => (err = e), - ) - if (err !== EMPTY_VALUE) return onError(err) - onNext(val) + + const onNext = (value: O | typeof SUSPENSE) => { + if (value === SUSPENSE) { + setState(source$.getValue) + } else if (!Object.is(value, prevVal)) { + setState(value) + } + prevVal = value + } + const onError = (error: any) => { + err = error + setState(() => { + throw error + }) + } + + let subscription = source$.subscribe(onNext, onError) + if (err !== EMPTY_VALUE) return + if (prevVal === EMPTY_VALUE) onNext(SUSPENSE) const t = subscription subscription = source$.subscribe(onNext, onError) t.unsubscribe() @@ -50,7 +37,5 @@ export const useObservable = ( return () => subscription.unsubscribe() }, [source$]) - const { type, payload } = state - if (type === Action.Value) return payload - throw payload + return state }