Skip to content

Commit 794f1e5

Browse files
committed
feat(bind): factory with nested keys
1 parent a801f81 commit 794f1e5

File tree

3 files changed

+86
-32
lines changed

3 files changed

+86
-32
lines changed

packages/core/src/bind/connectFactoryObservable.test.tsx

+22-18
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
import { bind } from "../"
2222
import { TestErrorBoundary } from "../test-helpers/TestErrorBoundary"
2323

24-
const wait = (ms: number) => new Promise(res => setTimeout(res, ms))
24+
const wait = (ms: number) => new Promise((res) => setTimeout(res, ms))
2525

2626
describe("connectFactoryObservable", () => {
2727
const originalError = console.error
@@ -86,24 +86,26 @@ describe("connectFactoryObservable", () => {
8686
const [
8787
useLatestNumber,
8888
latestNumber$,
89-
] = bind((id: number, value: number) =>
90-
concat(observable$, of(id + value)),
89+
] = bind((id: number, value: { val: number }) =>
90+
concat(observable$, of(id + value.val)),
9191
)
9292
expect(subscriberCount).toBe(0)
9393

94-
renderHook(() => useLatestNumber(1, 1))
94+
const first = { val: 1 }
95+
renderHook(() => useLatestNumber(1, first))
9596
expect(subscriberCount).toBe(1)
9697

97-
renderHook(() => useLatestNumber(1, 1))
98+
renderHook(() => useLatestNumber(1, first))
9899
expect(subscriberCount).toBe(1)
99100

100-
latestNumber$(1, 1).subscribe()
101+
latestNumber$(1, first).subscribe()
101102
expect(subscriberCount).toBe(1)
102103

103-
renderHook(() => useLatestNumber(1, 2))
104+
const second = { val: 2 }
105+
renderHook(() => useLatestNumber(1, second))
104106
expect(subscriberCount).toBe(2)
105107

106-
renderHook(() => useLatestNumber(2, 2))
108+
renderHook(() => useLatestNumber(2, second))
107109
expect(subscriberCount).toBe(3)
108110
})
109111

@@ -127,7 +129,7 @@ describe("connectFactoryObservable", () => {
127129

128130
it("suspends the component when the factory-observable hasn't emitted yet.", async () => {
129131
const [useDelayedNumber] = bind((x: number) => of(x).pipe(delay(50)))
130-
const Result: React.FC<{ input: number }> = p => (
132+
const Result: React.FC<{ input: number }> = (p) => (
131133
<div>Result {useDelayedNumber(p.input)}</div>
132134
)
133135
const TestSuspense: React.FC = () => {
@@ -137,7 +139,7 @@ describe("connectFactoryObservable", () => {
137139
<Suspense fallback={<span>Waiting</span>}>
138140
<Result input={input} />
139141
</Suspense>
140-
<button onClick={() => setInput(x => x + 1)}>increase</button>
142+
<button onClick={() => setInput((x) => x + 1)}>increase</button>
141143
</>
142144
)
143145
}
@@ -231,7 +233,7 @@ describe("connectFactoryObservable", () => {
231233
})
232234

233235
it("allows sync errors to be caught in error boundaries with suspense", () => {
234-
const errStream = new Observable(observer =>
236+
const errStream = new Observable((observer) =>
235237
observer.error("controlled error"),
236238
)
237239
const [useError] = bind((_: string) => errStream)
@@ -294,7 +296,7 @@ describe("connectFactoryObservable", () => {
294296
"key of the hook to an observable that throws synchronously",
295297
async () => {
296298
const normal$ = new Subject<string>()
297-
const errored$ = new Observable<string>(observer => {
299+
const errored$ = new Observable<string>((observer) => {
298300
observer.error("controlled error")
299301
})
300302

@@ -345,7 +347,9 @@ describe("connectFactoryObservable", () => {
345347
const valueStream = new BehaviorSubject(1)
346348
const [useValue, value$] = bind(() => valueStream)
347349
const [useError] = bind(() =>
348-
value$().pipe(switchMap(v => (v === 1 ? of(v) : throwError("error")))),
350+
value$().pipe(
351+
switchMap((v) => (v === 1 ? of(v) : throwError("error"))),
352+
),
349353
)
350354

351355
const ErrorComponent: FC = () => {
@@ -382,12 +386,12 @@ describe("connectFactoryObservable", () => {
382386
let diff = -1
383387
const [useLatestNumber, getShared] = bind((_: number) => {
384388
diff++
385-
return from([1, 2, 3, 4].map(val => val + diff))
389+
return from([1, 2, 3, 4].map((val) => val + diff))
386390
}, 0)
387391

388392
let latestValue1: number = 0
389393
let nUpdates = 0
390-
const sub1 = getShared(0).subscribe(x => {
394+
const sub1 = getShared(0).subscribe((x) => {
391395
latestValue1 = x
392396
nUpdates += 1
393397
})
@@ -400,7 +404,7 @@ describe("connectFactoryObservable", () => {
400404
expect(nUpdates).toBe(4)
401405

402406
let latestValue2: number = 0
403-
const sub2 = getShared(0).subscribe(x => {
407+
const sub2 = getShared(0).subscribe((x) => {
404408
latestValue2 = x
405409
nUpdates += 1
406410
})
@@ -409,7 +413,7 @@ describe("connectFactoryObservable", () => {
409413
expect(sub2.closed).toBe(true)
410414

411415
let latestValue3: number = 0
412-
const sub3 = getShared(0).subscribe(x => {
416+
const sub3 = getShared(0).subscribe((x) => {
413417
latestValue3 = x
414418
nUpdates += 1
415419
})
@@ -421,7 +425,7 @@ describe("connectFactoryObservable", () => {
421425
await wait(10)
422426

423427
let latestValue4: number = 0
424-
const sub4 = getShared(0).subscribe(x => {
428+
const sub4 = getShared(0).subscribe((x) => {
425429
latestValue4 = x
426430
nUpdates += 1
427431
})

packages/core/src/bind/connectFactoryObservable.ts

+56-12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,53 @@ import { useObservable } from "../internal/useObservable"
66
import { SUSPENSE } from "../SUSPENSE"
77
import { takeUntilComplete } from "../internal/take-until-complete"
88

9+
class NestedMap<K extends [], V extends Object> {
10+
private root: Map<K, any>
11+
constructor() {
12+
this.root = new Map()
13+
}
14+
15+
get(keys: K[]): V | undefined {
16+
let current: any = this.root
17+
for (let i = 0; i < keys.length; i++) {
18+
current = current.get(keys[i])
19+
if (!current) return undefined
20+
}
21+
return current
22+
}
23+
24+
set(keys: K[], value: V): void {
25+
let current: Map<K, any> = this.root
26+
let i
27+
for (i = 0; i < keys.length - 1; i++) {
28+
let nextCurrent = current.get(keys[i])
29+
if (!nextCurrent) {
30+
nextCurrent = new Map<K, any>()
31+
current.set(keys[i], nextCurrent)
32+
}
33+
current = nextCurrent
34+
}
35+
current.set(keys[i], value)
36+
}
37+
38+
delete(keys: K[]): void {
39+
const maps: Map<K, any>[] = [this.root]
40+
let current: Map<K, any> = this.root
41+
42+
for (let i = 0; i < keys.length - 1; i++) {
43+
maps.push((current = current.get(keys[i])))
44+
}
45+
46+
let mapIdx = maps.length - 1
47+
maps[mapIdx].delete(keys[mapIdx])
48+
49+
while (--mapIdx > -1 && maps[mapIdx].get(keys[mapIdx]).size === 0) {
50+
maps[mapIdx].delete(keys[mapIdx])
51+
}
52+
}
53+
}
54+
55+
const emptyInput = [0]
956
/**
1057
* Accepts: A factory function that returns an Observable.
1158
*
@@ -28,30 +75,27 @@ import { takeUntilComplete } from "../internal/take-until-complete"
2875
* subscription, then the hook will leverage React Suspense while it's waiting
2976
* for the first value.
3077
*/
31-
export default function connectFactoryObservable<
32-
A extends (number | string | boolean | null)[],
33-
O
34-
>(
78+
export default function connectFactoryObservable<A extends [], O>(
3579
getObservable: (...args: A) => Observable<O>,
3680
unsubscribeGraceTime: number,
3781
): [
3882
(...args: A) => Exclude<O, typeof SUSPENSE>,
3983
(...args: A) => Observable<O>,
4084
] {
41-
const cache = new Map<string, [Observable<O>, BehaviorObservable<O>]>()
85+
const cache = new NestedMap<A, [Observable<O>, BehaviorObservable<O>]>()
4286

4387
const getSharedObservables$ = (
44-
...input: A
88+
input: A,
4589
): [Observable<O>, BehaviorObservable<O>] => {
46-
const key = JSON.stringify(input)
47-
const cachedVal = cache.get(key)
90+
const keys = input.length > 0 ? input : (emptyInput as A)
91+
const cachedVal = cache.get(keys)
4892

4993
if (cachedVal !== undefined) {
5094
return cachedVal
5195
}
5296

5397
const sharedObservable$ = shareLatest(getObservable(...input), () => {
54-
cache.delete(key)
98+
cache.delete(keys)
5599
})
56100

57101
const reactObservable$ = reactEnhancer(
@@ -64,12 +108,12 @@ export default function connectFactoryObservable<
64108
reactObservable$,
65109
]
66110

67-
cache.set(key, result)
111+
cache.set(keys, result)
68112
return result
69113
}
70114

71115
return [
72-
(...input: A) => useObservable(getSharedObservables$(...input)[1]),
73-
(...input: A) => getSharedObservables$(...input)[0],
116+
(...input: A) => useObservable(getSharedObservables$(input)[1]),
117+
(...input: A) => getSharedObservables$(input)[0],
74118
]
75119
}

packages/core/src/bind/index.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,18 @@ export function bind<T>(
4646
* subscription, then the hook will leverage React Suspense while it's waiting
4747
* for the first value.
4848
*/
49-
export function bind<A extends (number | string | boolean | null)[], O>(
49+
export function bind<
50+
A extends (number | string | boolean | null | Object | Symbol)[],
51+
O
52+
>(
5053
getObservable: (...args: A) => Observable<O>,
5154
unsubscribeGraceTime?: number,
5255
): [(...args: A) => Exclude<O, typeof SUSPENSE>, (...args: A) => Observable<O>]
5356

54-
export function bind<A extends (number | string | boolean | null)[], O>(
57+
export function bind<
58+
A extends (number | string | boolean | null | Object | Symbol)[],
59+
O
60+
>(
5561
obs: ((...args: A) => Observable<O>) | Observable<O>,
5662
unsubscribeGraceTime = 200,
5763
) {

0 commit comments

Comments
 (0)