feat(bind): factory with nested keys

This commit is contained in:
Josep M Sobrepere Profitós 2020-08-15 17:17:12 +02:00 committed by Josep M Sobrepere
parent 031faad4a2
commit 8eac0e13ce
3 changed files with 86 additions and 32 deletions

View File

@ -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) => (
<div>Result {useDelayedNumber(p.input)}</div>
)
const TestSuspense: React.FC = () => {
@ -137,7 +139,7 @@ describe("connectFactoryObservable", () => {
<Suspense fallback={<span>Waiting</span>}>
<Result input={input} />
</Suspense>
<button onClick={() => setInput(x => x + 1)}>increase</button>
<button onClick={() => setInput((x) => x + 1)}>increase</button>
</>
)
}
@ -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<string>()
const errored$ = new Observable<string>(observer => {
const errored$ = new Observable<string>((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
})

View File

@ -6,6 +6,53 @@ import { useObservable } from "../internal/useObservable"
import { SUSPENSE } from "../SUSPENSE"
import { takeUntilComplete } from "../internal/take-until-complete"
class NestedMap<K extends [], V extends Object> {
private root: Map<K, any>
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<K, any> = this.root
let i
for (i = 0; i < keys.length - 1; i++) {
let nextCurrent = current.get(keys[i])
if (!nextCurrent) {
nextCurrent = new Map<K, any>()
current.set(keys[i], nextCurrent)
}
current = nextCurrent
}
current.set(keys[i], value)
}
delete(keys: K[]): void {
const maps: Map<K, any>[] = [this.root]
let current: Map<K, any> = 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<A extends [], O>(
getObservable: (...args: A) => Observable<O>,
unsubscribeGraceTime: number,
): [
(...args: A) => Exclude<O, typeof SUSPENSE>,
(...args: A) => Observable<O>,
] {
const cache = new Map<string, [Observable<O>, BehaviorObservable<O>]>()
const cache = new NestedMap<A, [Observable<O>, BehaviorObservable<O>]>()
const getSharedObservables$ = (
...input: A
input: A,
): [Observable<O>, BehaviorObservable<O>] => {
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],
]
}

View File

@ -46,12 +46,18 @@ export function bind<T>(
* subscription, then the hook will leverage React Suspense while it's waiting
* for the first value.
*/
export function bind<A extends (number | string | boolean | null)[], O>(
export function bind<
A extends (number | string | boolean | null | Object | Symbol)[],
O
>(
getObservable: (...args: A) => Observable<O>,
unsubscribeGraceTime?: number,
): [(...args: A) => Exclude<O, typeof SUSPENSE>, (...args: A) => Observable<O>]
export function bind<A extends (number | string | boolean | null)[], O>(
export function bind<
A extends (number | string | boolean | null | Object | Symbol)[],
O
>(
obs: ((...args: A) => Observable<O>) | Observable<O>,
unsubscribeGraceTime = 200,
) {