From 8eac0e13ce758403183cd5281bbd03d4576ebf12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josep=20M=20Sobrepere=20Profit=C3=B3s?= Date: Sat, 15 Aug 2020 17:17:12 +0200 Subject: [PATCH] feat(bind): factory with nested keys --- .../bind/connectFactoryObservable.test.tsx | 40 ++++++----- .../core/src/bind/connectFactoryObservable.ts | 68 +++++++++++++++---- packages/core/src/bind/index.ts | 10 ++- 3 files changed, 86 insertions(+), 32 deletions(-) diff --git a/packages/core/src/bind/connectFactoryObservable.test.tsx b/packages/core/src/bind/connectFactoryObservable.test.tsx index ac5e845..0a468b8 100644 --- a/packages/core/src/bind/connectFactoryObservable.test.tsx +++ b/packages/core/src/bind/connectFactoryObservable.test.tsx @@ -21,7 +21,7 @@ import { import { bind } from "../" import { TestErrorBoundary } from "../test-helpers/TestErrorBoundary" -const wait = (ms: number) => new Promise(res => setTimeout(res, ms)) +const wait = (ms: number) => new Promise((res) => setTimeout(res, ms)) describe("connectFactoryObservable", () => { const originalError = console.error @@ -86,24 +86,26 @@ describe("connectFactoryObservable", () => { const [ useLatestNumber, latestNumber$, - ] = bind((id: number, value: number) => - concat(observable$, of(id + value)), + ] = bind((id: number, value: { val: number }) => + concat(observable$, of(id + value.val)), ) expect(subscriberCount).toBe(0) - renderHook(() => useLatestNumber(1, 1)) + const first = { val: 1 } + renderHook(() => useLatestNumber(1, first)) expect(subscriberCount).toBe(1) - renderHook(() => useLatestNumber(1, 1)) + renderHook(() => useLatestNumber(1, first)) expect(subscriberCount).toBe(1) - latestNumber$(1, 1).subscribe() + latestNumber$(1, first).subscribe() expect(subscriberCount).toBe(1) - renderHook(() => useLatestNumber(1, 2)) + const second = { val: 2 } + renderHook(() => useLatestNumber(1, second)) expect(subscriberCount).toBe(2) - renderHook(() => useLatestNumber(2, 2)) + renderHook(() => useLatestNumber(2, second)) expect(subscriberCount).toBe(3) }) @@ -127,7 +129,7 @@ describe("connectFactoryObservable", () => { it("suspends the component when the factory-observable hasn't emitted yet.", async () => { const [useDelayedNumber] = bind((x: number) => of(x).pipe(delay(50))) - const Result: React.FC<{ input: number }> = p => ( + const Result: React.FC<{ input: number }> = (p) => (
Result {useDelayedNumber(p.input)}
) const TestSuspense: React.FC = () => { @@ -137,7 +139,7 @@ describe("connectFactoryObservable", () => { Waiting}> - + ) } @@ -231,7 +233,7 @@ describe("connectFactoryObservable", () => { }) it("allows sync errors to be caught in error boundaries with suspense", () => { - const errStream = new Observable(observer => + const errStream = new Observable((observer) => observer.error("controlled error"), ) const [useError] = bind((_: string) => errStream) @@ -294,7 +296,7 @@ describe("connectFactoryObservable", () => { "key of the hook to an observable that throws synchronously", async () => { const normal$ = new Subject() - const errored$ = new Observable(observer => { + const errored$ = new Observable((observer) => { observer.error("controlled error") }) @@ -345,7 +347,9 @@ describe("connectFactoryObservable", () => { const valueStream = new BehaviorSubject(1) const [useValue, value$] = bind(() => valueStream) const [useError] = bind(() => - value$().pipe(switchMap(v => (v === 1 ? of(v) : throwError("error")))), + value$().pipe( + switchMap((v) => (v === 1 ? of(v) : throwError("error"))), + ), ) const ErrorComponent: FC = () => { @@ -382,12 +386,12 @@ describe("connectFactoryObservable", () => { let diff = -1 const [useLatestNumber, getShared] = bind((_: number) => { diff++ - return from([1, 2, 3, 4].map(val => val + diff)) + return from([1, 2, 3, 4].map((val) => val + diff)) }, 0) let latestValue1: number = 0 let nUpdates = 0 - const sub1 = getShared(0).subscribe(x => { + const sub1 = getShared(0).subscribe((x) => { latestValue1 = x nUpdates += 1 }) @@ -400,7 +404,7 @@ describe("connectFactoryObservable", () => { expect(nUpdates).toBe(4) let latestValue2: number = 0 - const sub2 = getShared(0).subscribe(x => { + const sub2 = getShared(0).subscribe((x) => { latestValue2 = x nUpdates += 1 }) @@ -409,7 +413,7 @@ describe("connectFactoryObservable", () => { expect(sub2.closed).toBe(true) let latestValue3: number = 0 - const sub3 = getShared(0).subscribe(x => { + const sub3 = getShared(0).subscribe((x) => { latestValue3 = x nUpdates += 1 }) @@ -421,7 +425,7 @@ describe("connectFactoryObservable", () => { await wait(10) let latestValue4: number = 0 - const sub4 = getShared(0).subscribe(x => { + const sub4 = getShared(0).subscribe((x) => { latestValue4 = x nUpdates += 1 }) diff --git a/packages/core/src/bind/connectFactoryObservable.ts b/packages/core/src/bind/connectFactoryObservable.ts index c1b1b0e..945ff34 100644 --- a/packages/core/src/bind/connectFactoryObservable.ts +++ b/packages/core/src/bind/connectFactoryObservable.ts @@ -6,6 +6,53 @@ import { useObservable } from "../internal/useObservable" import { SUSPENSE } from "../SUSPENSE" import { takeUntilComplete } from "../internal/take-until-complete" +class NestedMap { + private root: Map + constructor() { + this.root = new Map() + } + + get(keys: K[]): V | undefined { + let current: any = this.root + for (let i = 0; i < keys.length; i++) { + current = current.get(keys[i]) + if (!current) return undefined + } + return current + } + + set(keys: K[], value: V): void { + let current: Map = this.root + let i + for (i = 0; i < keys.length - 1; i++) { + let nextCurrent = current.get(keys[i]) + if (!nextCurrent) { + nextCurrent = new Map() + current.set(keys[i], nextCurrent) + } + current = nextCurrent + } + current.set(keys[i], value) + } + + delete(keys: K[]): void { + const maps: Map[] = [this.root] + let current: Map = this.root + + for (let i = 0; i < keys.length - 1; i++) { + maps.push((current = current.get(keys[i]))) + } + + let mapIdx = maps.length - 1 + maps[mapIdx].delete(keys[mapIdx]) + + while (--mapIdx > -1 && maps[mapIdx].get(keys[mapIdx]).size === 0) { + maps[mapIdx].delete(keys[mapIdx]) + } + } +} + +const emptyInput = [0] /** * Accepts: A factory function that returns an Observable. * @@ -28,30 +75,27 @@ import { takeUntilComplete } from "../internal/take-until-complete" * subscription, then the hook will leverage React Suspense while it's waiting * for the first value. */ -export default function connectFactoryObservable< - A extends (number | string | boolean | null)[], - O ->( +export default function connectFactoryObservable( getObservable: (...args: A) => Observable, unsubscribeGraceTime: number, ): [ (...args: A) => Exclude, (...args: A) => Observable, ] { - const cache = new Map, BehaviorObservable]>() + const cache = new NestedMap, BehaviorObservable]>() const getSharedObservables$ = ( - ...input: A + input: A, ): [Observable, BehaviorObservable] => { - const key = JSON.stringify(input) - const cachedVal = cache.get(key) + const keys = input.length > 0 ? input : (emptyInput as A) + const cachedVal = cache.get(keys) if (cachedVal !== undefined) { return cachedVal } const sharedObservable$ = shareLatest(getObservable(...input), () => { - cache.delete(key) + cache.delete(keys) }) const reactObservable$ = reactEnhancer( @@ -64,12 +108,12 @@ export default function connectFactoryObservable< reactObservable$, ] - cache.set(key, result) + cache.set(keys, result) return result } return [ - (...input: A) => useObservable(getSharedObservables$(...input)[1]), - (...input: A) => getSharedObservables$(...input)[0], + (...input: A) => useObservable(getSharedObservables$(input)[1]), + (...input: A) => getSharedObservables$(input)[0], ] } diff --git a/packages/core/src/bind/index.ts b/packages/core/src/bind/index.ts index a957c4f..3fb9b67 100644 --- a/packages/core/src/bind/index.ts +++ b/packages/core/src/bind/index.ts @@ -46,12 +46,18 @@ export function bind( * subscription, then the hook will leverage React Suspense while it's waiting * for the first value. */ -export function bind( +export function bind< + A extends (number | string | boolean | null | Object | Symbol)[], + O +>( getObservable: (...args: A) => Observable, unsubscribeGraceTime?: number, ): [(...args: A) => Exclude, (...args: A) => Observable] -export function bind( +export function bind< + A extends (number | string | boolean | null | Object | Symbol)[], + O +>( obs: ((...args: A) => Observable) | Observable, unsubscribeGraceTime = 200, ) {