mirror of
https://github.com/re-rxjs/react-rxjs.git
synced 2025-12-08 18:01:51 +00:00
feat(bind): factory with nested keys
This commit is contained in:
parent
031faad4a2
commit
8eac0e13ce
@ -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
|
||||
})
|
||||
|
||||
@ -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],
|
||||
]
|
||||
}
|
||||
|
||||
@ -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,
|
||||
) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user