feat(bind): nested keys accept optional args

This commit is contained in:
Josep M Sobrepere Profitós 2020-08-16 01:43:27 +02:00 committed by Josep M Sobrepere
parent 8eac0e13ce
commit 2d58fbab32
3 changed files with 77 additions and 74 deletions

View File

@ -127,6 +127,13 @@ describe("connectFactoryObservable", () => {
expect(result.current).toBe(2)
})
it("handles optional args correctly", () => {
const [, getNumber$] = bind((x: number, y?: number) => of(x + (y ?? 0)))
expect(getNumber$(5)).toBe(getNumber$(5, undefined))
expect(getNumber$(6, undefined)).toBe(getNumber$(6))
})
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) => (

View File

@ -6,6 +6,74 @@ import { useObservable } from "../internal/useObservable"
import { SUSPENSE } from "../SUSPENSE"
import { takeUntilComplete } from "../internal/take-until-complete"
/**
* Accepts: A factory function that returns an Observable.
*
* Returns [1, 2]
* 1. A React Hook function with the same parameters as the factory function.
* This hook will yield the latest update from the observable returned from
* the factory function.
* 2. A `sharedLatest` version of the observable generated by the factory
* function that can be used for composing other streams that depend on it.
* The shared subscription is closed as soon as there are no subscribers to
* that observable.
*
* @param getObservable Factory of observables. The arguments of this function
* will be the ones used in the hook.
* @param unsubscribeGraceTime (= 200): Amount of time in ms that the shared
* observable should wait before unsubscribing from the source observable when
* there are no new subscribers.
*
* @remarks If the Observable doesn't synchronously emit a value upon the first
* subscription, then the hook will leverage React Suspense while it's waiting
* for the first value.
*/
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 NestedMap<A, [Observable<O>, BehaviorObservable<O>]>()
const getSharedObservables$ = (
input: A,
): [Observable<O>, BehaviorObservable<O>] => {
for (let i = input.length - 1; input[i] === undefined && i > -1; i--) {
input.splice(-1)
}
const keys = ([input.length, ...input] as any) as A
const cachedVal = cache.get(keys)
if (cachedVal !== undefined) {
return cachedVal
}
const sharedObservable$ = shareLatest(getObservable(...input), () => {
cache.delete(keys)
})
const reactObservable$ = reactEnhancer(
sharedObservable$,
unsubscribeGraceTime,
)
const result: [Observable<O>, BehaviorObservable<O>] = [
takeUntilComplete(sharedObservable$),
reactObservable$,
]
cache.set(keys, result)
return result
}
return [
(...input: A) => useObservable(getSharedObservables$(input)[1]),
(...input: A) => getSharedObservables$(input)[0],
]
}
class NestedMap<K extends [], V extends Object> {
private root: Map<K, any>
constructor() {
@ -51,69 +119,3 @@ class NestedMap<K extends [], V extends Object> {
}
}
}
const emptyInput = [0]
/**
* Accepts: A factory function that returns an Observable.
*
* Returns [1, 2]
* 1. A React Hook function with the same parameters as the factory function.
* This hook will yield the latest update from the observable returned from
* the factory function.
* 2. A `sharedLatest` version of the observable generated by the factory
* function that can be used for composing other streams that depend on it.
* The shared subscription is closed as soon as there are no subscribers to
* that observable.
*
* @param getObservable Factory of observables. The arguments of this function
* will be the ones used in the hook.
* @param unsubscribeGraceTime (= 200): Amount of time in ms that the shared
* observable should wait before unsubscribing from the source observable when
* there are no new subscribers.
*
* @remarks If the Observable doesn't synchronously emit a value upon the first
* subscription, then the hook will leverage React Suspense while it's waiting
* for the first value.
*/
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 NestedMap<A, [Observable<O>, BehaviorObservable<O>]>()
const getSharedObservables$ = (
input: A,
): [Observable<O>, BehaviorObservable<O>] => {
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(keys)
})
const reactObservable$ = reactEnhancer(
sharedObservable$,
unsubscribeGraceTime,
)
const result: [Observable<O>, BehaviorObservable<O>] = [
takeUntilComplete(sharedObservable$),
reactObservable$,
]
cache.set(keys, result)
return result
}
return [
(...input: A) => useObservable(getSharedObservables$(input)[1]),
(...input: A) => getSharedObservables$(input)[0],
]
}

View File

@ -46,18 +46,12 @@ 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 | Object | Symbol)[],
O
>(
export function bind<A extends unknown[], 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 | Object | Symbol)[],
O
>(
export function bind<A extends unknown[], O>(
obs: ((...args: A) => Observable<O>) | Observable<O>,
unsubscribeGraceTime = 200,
) {