@livestore/react 0.4.0-dev.9 → 0.5.0-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/StoreRegistryContext.d.ts +56 -0
- package/dist/StoreRegistryContext.d.ts.map +1 -0
- package/dist/StoreRegistryContext.js +61 -0
- package/dist/StoreRegistryContext.js.map +1 -0
- package/dist/__tests__/fixture.d.ts +12 -283
- package/dist/__tests__/fixture.d.ts.map +1 -1
- package/dist/__tests__/fixture.js +12 -86
- package/dist/__tests__/fixture.js.map +1 -1
- package/dist/experimental/components/LiveList.d.ts +4 -2
- package/dist/experimental/components/LiveList.d.ts.map +1 -1
- package/dist/experimental/components/LiveList.js +11 -9
- package/dist/experimental/components/LiveList.js.map +1 -1
- package/dist/mod.d.ts +8 -5
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +6 -4
- package/dist/mod.js.map +1 -1
- package/dist/useClientDocument.d.ts +12 -4
- package/dist/useClientDocument.d.ts.map +1 -1
- package/dist/useClientDocument.js +4 -18
- package/dist/useClientDocument.js.map +1 -1
- package/dist/useClientDocument.test.js +28 -20
- package/dist/useClientDocument.test.js.map +1 -1
- package/dist/useQuery.d.ts +29 -8
- package/dist/useQuery.d.ts.map +1 -1
- package/dist/useQuery.js +43 -70
- package/dist/useQuery.js.map +1 -1
- package/dist/useQuery.test.js +67 -23
- package/dist/useQuery.test.js.map +1 -1
- package/dist/useRcResource.d.ts +1 -1
- package/dist/useRcResource.d.ts.map +1 -1
- package/dist/useRcResource.js +42 -35
- package/dist/useRcResource.js.map +1 -1
- package/dist/useRcResource.test.js +73 -50
- package/dist/useRcResource.test.js.map +1 -1
- package/dist/useStore.d.ts +74 -7
- package/dist/useStore.d.ts.map +1 -1
- package/dist/useStore.js +82 -16
- package/dist/useStore.js.map +1 -1
- package/dist/useStore.test.d.ts +2 -0
- package/dist/useStore.test.d.ts.map +1 -0
- package/dist/useStore.test.js +242 -0
- package/dist/useStore.test.js.map +1 -0
- package/dist/useSyncStatus.d.ts +22 -0
- package/dist/useSyncStatus.d.ts.map +1 -0
- package/dist/useSyncStatus.js +28 -0
- package/dist/useSyncStatus.js.map +1 -0
- package/package.json +60 -27
- package/src/StoreRegistryContext.tsx +70 -0
- package/src/__snapshots__/useClientDocument.test.tsx.snap +152 -78
- package/src/__snapshots__/useQuery.test.tsx.snap +12 -12
- package/src/__tests__/fixture.tsx +32 -135
- package/src/experimental/components/LiveList.tsx +25 -11
- package/src/mod.ts +8 -12
- package/src/useClientDocument.test.tsx +38 -23
- package/src/useClientDocument.ts +19 -38
- package/src/useQuery.test.tsx +110 -23
- package/src/useQuery.ts +74 -89
- package/src/useRcResource.test.tsx +116 -59
- package/src/useRcResource.ts +52 -38
- package/src/useStore.test.tsx +357 -0
- package/src/useStore.ts +115 -22
- package/src/useSyncStatus.ts +34 -0
- package/dist/LiveStoreContext.d.ts +0 -13
- package/dist/LiveStoreContext.d.ts.map +0 -1
- package/dist/LiveStoreContext.js +0 -3
- package/dist/LiveStoreContext.js.map +0 -1
- package/dist/LiveStoreProvider.d.ts +0 -65
- package/dist/LiveStoreProvider.d.ts.map +0 -1
- package/dist/LiveStoreProvider.js +0 -221
- package/dist/LiveStoreProvider.js.map +0 -1
- package/dist/LiveStoreProvider.test.d.ts +0 -2
- package/dist/LiveStoreProvider.test.d.ts.map +0 -1
- package/dist/LiveStoreProvider.test.js +0 -117
- package/dist/LiveStoreProvider.test.js.map +0 -1
- package/dist/utils/stack-info.d.ts +0 -4
- package/dist/utils/stack-info.d.ts.map +0 -1
- package/dist/utils/stack-info.js +0 -10
- package/dist/utils/stack-info.js.map +0 -1
- package/src/LiveStoreContext.ts +0 -14
- package/src/LiveStoreProvider.test.tsx +0 -248
- package/src/LiveStoreProvider.tsx +0 -413
- package/src/ambient.d.ts +0 -1
- package/src/utils/stack-info.ts +0 -13
|
@@ -2,20 +2,24 @@ import * as ReactTesting from '@testing-library/react'
|
|
|
2
2
|
import * as React from 'react'
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
4
|
|
|
5
|
-
import { __resetUseRcResourceCache, useRcResource } from './useRcResource.
|
|
5
|
+
import { __resetUseRcResourceCache, useRcResource } from './useRcResource.ts'
|
|
6
6
|
|
|
7
|
+
/** Verifies: LS.SYS.INT.REACT-R03, LS.SYS.INT.REACT-R05 */
|
|
7
8
|
describe.each([{ strictMode: true }, { strictMode: false }])('useRcResource (strictMode=%s)', ({ strictMode }) => {
|
|
8
9
|
beforeEach(() => {
|
|
9
10
|
__resetUseRcResourceCache()
|
|
10
11
|
})
|
|
11
12
|
|
|
12
|
-
const wrapper = strictMode ? React.StrictMode : React.Fragment
|
|
13
|
+
const wrapper = strictMode === true ? React.StrictMode : React.Fragment
|
|
13
14
|
|
|
14
15
|
it('should create a stateful entity using make and call cleanup on unmount', () => {
|
|
16
|
+
const scope = {}
|
|
15
17
|
const makeSpy = vi.fn(() => Symbol('statefulResource'))
|
|
16
18
|
const cleanupSpy = vi.fn()
|
|
17
19
|
|
|
18
|
-
const { result, unmount } = ReactTesting.renderHook(() => useRcResource('key-1', makeSpy, cleanupSpy), {
|
|
20
|
+
const { result, unmount } = ReactTesting.renderHook(() => useRcResource(scope, 'key-1', makeSpy, cleanupSpy), {
|
|
21
|
+
wrapper,
|
|
22
|
+
})
|
|
19
23
|
|
|
20
24
|
expect(makeSpy).toHaveBeenCalledTimes(1)
|
|
21
25
|
expect(result.current).toBeDefined()
|
|
@@ -26,11 +30,12 @@ describe.each([{ strictMode: true }, { strictMode: false }])('useRcResource (str
|
|
|
26
30
|
})
|
|
27
31
|
|
|
28
32
|
it('should reuse the same entity when the key remains unchanged', () => {
|
|
33
|
+
const scope = {}
|
|
29
34
|
const makeSpy = vi.fn(() => Symbol('statefulResource'))
|
|
30
35
|
const cleanupSpy = vi.fn()
|
|
31
36
|
|
|
32
37
|
const { result, rerender, unmount } = ReactTesting.renderHook(
|
|
33
|
-
({ key }) => useRcResource(key, makeSpy, cleanupSpy),
|
|
38
|
+
({ key }) => useRcResource(scope, key, makeSpy, cleanupSpy),
|
|
34
39
|
{ initialProps: { key: 'consistent-key' }, wrapper },
|
|
35
40
|
)
|
|
36
41
|
|
|
@@ -48,11 +53,12 @@ describe.each([{ strictMode: true }, { strictMode: false }])('useRcResource (str
|
|
|
48
53
|
})
|
|
49
54
|
|
|
50
55
|
it('should dispose the previous instance when the key changes', () => {
|
|
56
|
+
const scope = {}
|
|
51
57
|
const makeSpy = vi.fn(() => Symbol('statefulResource'))
|
|
52
58
|
const cleanupSpy = vi.fn()
|
|
53
59
|
|
|
54
60
|
const { result, rerender, unmount } = ReactTesting.renderHook(
|
|
55
|
-
({ key }) => useRcResource(key, makeSpy, cleanupSpy),
|
|
61
|
+
({ key }) => useRcResource(scope, key, makeSpy, cleanupSpy),
|
|
56
62
|
{ initialProps: { key: 'a' }, wrapper },
|
|
57
63
|
)
|
|
58
64
|
|
|
@@ -71,18 +77,18 @@ describe.each([{ strictMode: true }, { strictMode: false }])('useRcResource (str
|
|
|
71
77
|
})
|
|
72
78
|
|
|
73
79
|
it('should not dispose the entity until all consumers unmount', () => {
|
|
80
|
+
const scope = {}
|
|
74
81
|
const makeSpy = vi.fn(() => Symbol('statefulResource'))
|
|
75
82
|
const cleanupSpy = vi.fn()
|
|
76
83
|
|
|
77
|
-
// Simulate two consumers using the same key independently.
|
|
78
|
-
const { unmount: unmount1 } = ReactTesting.renderHook(
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
// Simulate two consumers using the same (scope, key) pair independently.
|
|
85
|
+
const { unmount: unmount1 } = ReactTesting.renderHook(
|
|
86
|
+
() => useRcResource(scope, 'shared-key', makeSpy, cleanupSpy),
|
|
87
|
+
{ wrapper },
|
|
88
|
+
)
|
|
81
89
|
const { unmount: unmount2, result } = ReactTesting.renderHook(
|
|
82
|
-
() => useRcResource('shared-key', makeSpy, cleanupSpy),
|
|
83
|
-
{
|
|
84
|
-
wrapper,
|
|
85
|
-
},
|
|
90
|
+
() => useRcResource(scope, 'shared-key', makeSpy, cleanupSpy),
|
|
91
|
+
{ wrapper },
|
|
86
92
|
)
|
|
87
93
|
|
|
88
94
|
expect(result.current).toBeDefined()
|
|
@@ -98,10 +104,11 @@ describe.each([{ strictMode: true }, { strictMode: false }])('useRcResource (str
|
|
|
98
104
|
})
|
|
99
105
|
|
|
100
106
|
it('should handle rapid key changes correctly', () => {
|
|
107
|
+
const scope = {}
|
|
101
108
|
const makeSpy = vi.fn(() => Symbol('statefulResource'))
|
|
102
109
|
const cleanupSpy = vi.fn()
|
|
103
110
|
|
|
104
|
-
const { rerender, unmount } = ReactTesting.renderHook(({ key }) => useRcResource(key, makeSpy, cleanupSpy), {
|
|
111
|
+
const { rerender, unmount } = ReactTesting.renderHook(({ key }) => useRcResource(scope, key, makeSpy, cleanupSpy), {
|
|
105
112
|
initialProps: { key: '1' },
|
|
106
113
|
wrapper,
|
|
107
114
|
})
|
|
@@ -119,49 +126,99 @@ describe.each([{ strictMode: true }, { strictMode: false }])('useRcResource (str
|
|
|
119
126
|
// Unmounting the final consumer disposes the key '3' instance.
|
|
120
127
|
expect(cleanupSpy).toHaveBeenCalledTimes(3)
|
|
121
128
|
})
|
|
122
|
-
})
|
|
123
129
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
130
|
+
it('should isolate entities created with the same key but different scopes', () => {
|
|
131
|
+
const scopeA = { tag: 'A' }
|
|
132
|
+
const scopeB = { tag: 'B' }
|
|
133
|
+
const makeSpy = vi.fn(() => Symbol('statefulResource'))
|
|
134
|
+
const cleanupSpy = vi.fn()
|
|
135
|
+
|
|
136
|
+
const { result: resultA } = ReactTesting.renderHook(
|
|
137
|
+
() => useRcResource(scopeA, 'shared-key', makeSpy, cleanupSpy),
|
|
138
|
+
{ wrapper },
|
|
139
|
+
)
|
|
140
|
+
const { result: resultB } = ReactTesting.renderHook(
|
|
141
|
+
() => useRcResource(scopeB, 'shared-key', makeSpy, cleanupSpy),
|
|
142
|
+
{ wrapper },
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
expect(resultA.current).not.toBe(resultB.current)
|
|
146
|
+
expect(makeSpy).toHaveBeenCalledTimes(2)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should dispose the previous entity when the scope changes (key unchanged)', () => {
|
|
150
|
+
const scopeA = { tag: 'A' }
|
|
151
|
+
const scopeB = { tag: 'B' }
|
|
152
|
+
const makeSpy = vi.fn(() => Symbol('statefulResource'))
|
|
153
|
+
const cleanupSpy = vi.fn()
|
|
154
|
+
|
|
155
|
+
const { result, rerender, unmount } = ReactTesting.renderHook(
|
|
156
|
+
({ scope }) => useRcResource(scope, 'k', makeSpy, cleanupSpy),
|
|
157
|
+
{ initialProps: { scope: scopeA }, wrapper },
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const instanceA = result.current
|
|
161
|
+
expect(makeSpy).toHaveBeenCalledTimes(1)
|
|
162
|
+
|
|
163
|
+
rerender({ scope: scopeB })
|
|
164
|
+
const instanceB = result.current
|
|
165
|
+
|
|
166
|
+
expect(instanceA).not.toBe(instanceB)
|
|
167
|
+
expect(makeSpy).toHaveBeenCalledTimes(2)
|
|
168
|
+
// The scopeA entry's last consumer left when we switched scopes → cleaned up.
|
|
169
|
+
expect(cleanupSpy).toHaveBeenCalledTimes(1)
|
|
170
|
+
|
|
171
|
+
unmount()
|
|
172
|
+
expect(cleanupSpy).toHaveBeenCalledTimes(2)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('should not reuse a cached entity after the scope is replaced', () => {
|
|
176
|
+
const makeSpy = vi.fn(() => Symbol('statefulResource'))
|
|
177
|
+
const cleanupSpy = vi.fn()
|
|
178
|
+
|
|
179
|
+
const scope1 = {}
|
|
180
|
+
const { result: result1, unmount: unmount1 } = ReactTesting.renderHook(
|
|
181
|
+
() => useRcResource(scope1, 'k', makeSpy, cleanupSpy),
|
|
182
|
+
{ wrapper },
|
|
183
|
+
)
|
|
184
|
+
const instance1 = result1.current
|
|
185
|
+
unmount1()
|
|
186
|
+
expect(cleanupSpy).toHaveBeenCalledTimes(1)
|
|
187
|
+
|
|
188
|
+
// Fresh scope, same string key — must NOT reuse the (already-disposed) entry.
|
|
189
|
+
const scope2 = {}
|
|
190
|
+
const { result: result2, unmount: unmount2 } = ReactTesting.renderHook(
|
|
191
|
+
() => useRcResource(scope2, 'k', makeSpy, cleanupSpy),
|
|
192
|
+
{ wrapper },
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
expect(result2.current).not.toBe(instance1)
|
|
196
|
+
expect(makeSpy).toHaveBeenCalledTimes(2)
|
|
197
|
+
|
|
198
|
+
unmount2()
|
|
199
|
+
expect(cleanupSpy).toHaveBeenCalledTimes(2)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('should share the entity across components within the same scope', () => {
|
|
203
|
+
const scope = {}
|
|
204
|
+
const makeSpy = vi.fn(() => Symbol('statefulResource'))
|
|
205
|
+
const cleanupSpy = vi.fn()
|
|
206
|
+
|
|
207
|
+
const { result: r1, unmount: unmount1 } = ReactTesting.renderHook(
|
|
208
|
+
() => useRcResource(scope, 'k', makeSpy, cleanupSpy),
|
|
209
|
+
{ wrapper },
|
|
210
|
+
)
|
|
211
|
+
const { result: r2, unmount: unmount2 } = ReactTesting.renderHook(
|
|
212
|
+
() => useRcResource(scope, 'k', makeSpy, cleanupSpy),
|
|
213
|
+
{ wrapper },
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
expect(r1.current).toBe(r2.current)
|
|
217
|
+
expect(makeSpy).toHaveBeenCalledTimes(1)
|
|
218
|
+
|
|
219
|
+
unmount1()
|
|
220
|
+
expect(cleanupSpy).not.toHaveBeenCalled()
|
|
221
|
+
unmount2()
|
|
222
|
+
expect(cleanupSpy).toHaveBeenCalledTimes(1)
|
|
223
|
+
})
|
|
224
|
+
})
|
package/src/useRcResource.ts
CHANGED
|
@@ -25,10 +25,10 @@ import * as React from 'react'
|
|
|
25
25
|
* - Upon component unmount, the reference count is decremented, leading to disposal (via the `dispose` function)
|
|
26
26
|
* if the reference count drops to zero. An unmount is either detected via React's `useEffect` callback or
|
|
27
27
|
* in the useMemo hook when the key changes.
|
|
28
|
-
*
|
|
28
|
+
*
|
|
29
29
|
* Why this is needed in LiveStore:
|
|
30
30
|
* Let's first take a look at the "trivial implementation":
|
|
31
|
-
*
|
|
31
|
+
*
|
|
32
32
|
* ```ts
|
|
33
33
|
* const useSimpleResource = <T>(create: () => T, dispose: (resource: T) => void) => {
|
|
34
34
|
* const val = React.useMemo(() => create(), [create])
|
|
@@ -42,7 +42,7 @@ import * as React from 'react'
|
|
|
42
42
|
* return val
|
|
43
43
|
* }
|
|
44
44
|
* ```
|
|
45
|
-
*
|
|
45
|
+
*
|
|
46
46
|
* LiveStore uses this hook to create LiveQuery instances which are stateful and must not be leaked.
|
|
47
47
|
* The simple implementation above would leak the LiveQuery instance if the component is unmounted or props change.
|
|
48
48
|
*
|
|
@@ -72,87 +72,87 @@ import * as React from 'react'
|
|
|
72
72
|
* @returns The stateful entity corresponding to the provided key.
|
|
73
73
|
*/
|
|
74
74
|
export const useRcResource = <T>(
|
|
75
|
+
scope: object,
|
|
75
76
|
key: string,
|
|
76
77
|
create: () => T,
|
|
77
78
|
dispose: (resource: NoInfer<T>) => void,
|
|
78
79
|
_options?: { debugPrint?: (resource: NoInfer<T>) => ReadonlyArray<any> },
|
|
79
80
|
): T => {
|
|
80
81
|
const keyRef = React.useRef<string | undefined>(undefined)
|
|
82
|
+
const scopeRef = React.useRef<object | undefined>(undefined)
|
|
81
83
|
const didDisposeInMemo = React.useRef(false)
|
|
84
|
+
const createRef = React.useRef(create)
|
|
85
|
+
const disposeRef = React.useRef(dispose)
|
|
82
86
|
|
|
83
|
-
|
|
87
|
+
createRef.current = create
|
|
88
|
+
disposeRef.current = dispose
|
|
89
|
+
|
|
90
|
+
// oxlint-disable-next-line react/exhaustive-deps -- dependencies are deliberately limited to `scope` and `key` to avoid unintended re-creations
|
|
84
91
|
const resource = React.useMemo(() => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const cachedItem =
|
|
92
|
+
const bucket = getBucket(scope)
|
|
93
|
+
|
|
94
|
+
if (didDisposeInMemo.current === true) {
|
|
95
|
+
const cachedItem = bucket.get(key)
|
|
89
96
|
if (cachedItem !== undefined && cachedItem._tag === 'active') {
|
|
90
97
|
return cachedItem.resource
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
100
|
|
|
94
|
-
// Check if the key has changed (or is undefined)
|
|
95
|
-
if (keyRef.current !== undefined && keyRef.current !== key) {
|
|
96
|
-
// If the key has changed, decrement the reference on the previous key
|
|
101
|
+
// Check if the (scope, key) pair has changed (or is undefined)
|
|
102
|
+
if (keyRef.current !== undefined && (keyRef.current !== key || scopeRef.current !== scope)) {
|
|
97
103
|
const previousKey = keyRef.current
|
|
98
|
-
|
|
104
|
+
// scopeRef.current is set together with keyRef.current below, so it's defined here.
|
|
105
|
+
const previousBucket = getBucket(scopeRef.current!)
|
|
106
|
+
const cachedItemForPreviousKey = previousBucket.get(previousKey)
|
|
99
107
|
if (cachedItemForPreviousKey !== undefined && cachedItemForPreviousKey._tag === 'active') {
|
|
100
|
-
// previousKeyRef.current = previousKey
|
|
101
108
|
cachedItemForPreviousKey.rc--
|
|
102
109
|
|
|
103
|
-
// console.debug('useMemo', key, 'rc--', previousKey, cachedItemForPreviousKey.rc)
|
|
104
|
-
|
|
105
110
|
if (cachedItemForPreviousKey.rc === 0) {
|
|
106
111
|
// Clean up the stateful resource if no longer referenced
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
disposeRef.current(cachedItemForPreviousKey.resource)
|
|
113
|
+
previousBucket.set(previousKey, { _tag: 'destroyed' })
|
|
109
114
|
didDisposeInMemo.current = true
|
|
110
115
|
}
|
|
111
116
|
}
|
|
112
117
|
}
|
|
113
118
|
|
|
114
|
-
const cachedItem =
|
|
119
|
+
const cachedItem = bucket.get(key)
|
|
115
120
|
if (cachedItem !== undefined && cachedItem._tag === 'active') {
|
|
116
121
|
// In React Strict Mode, the `useMemo` hook is called multiple times,
|
|
117
122
|
// so we only increment the reference from the first call for this component.
|
|
118
123
|
cachedItem.rc++
|
|
119
|
-
// console.debug('rc++', cachedItem.rc, ...(_options?.debugPrint?.(cachedItem.resource) ?? []))
|
|
120
|
-
|
|
121
124
|
return cachedItem.resource
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
// Create a new stateful resource if not cached
|
|
125
|
-
const resource =
|
|
126
|
-
|
|
128
|
+
const resource = createRef.current()
|
|
129
|
+
bucket.set(key, { _tag: 'active', rc: 1, resource })
|
|
127
130
|
return resource
|
|
128
|
-
}, [key])
|
|
131
|
+
}, [scope, key])
|
|
129
132
|
|
|
130
|
-
//
|
|
133
|
+
// oxlint-disable-next-line react/exhaustive-deps -- `disposeRef` keeps the latest dispose function without re-running cleanup
|
|
131
134
|
React.useEffect(() => {
|
|
132
135
|
return () => {
|
|
133
|
-
if (didDisposeInMemo.current) {
|
|
134
|
-
// console.debug('unmount', keyRef.current, 'skip')
|
|
136
|
+
if (didDisposeInMemo.current === true) {
|
|
135
137
|
didDisposeInMemo.current = false
|
|
136
138
|
return
|
|
137
139
|
}
|
|
138
140
|
|
|
139
|
-
|
|
140
|
-
const cachedItem =
|
|
141
|
-
// If the stateful resource is already cleaned up, do nothing.
|
|
141
|
+
const bucket = getBucket(scope)
|
|
142
|
+
const cachedItem = bucket.get(key)
|
|
142
143
|
if (cachedItem === undefined || cachedItem._tag === 'destroyed') return
|
|
143
144
|
|
|
144
145
|
cachedItem.rc--
|
|
145
146
|
|
|
146
|
-
// console.debug('rc--', cachedItem.rc, ...(_options?.debugPrint?.(cachedItem.resource) ?? []))
|
|
147
|
-
|
|
148
147
|
if (cachedItem.rc === 0) {
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
disposeRef.current(cachedItem.resource)
|
|
149
|
+
bucket.delete(key)
|
|
151
150
|
}
|
|
152
151
|
}
|
|
153
|
-
}, [key])
|
|
152
|
+
}, [scope, key])
|
|
154
153
|
|
|
155
154
|
keyRef.current = key
|
|
155
|
+
scopeRef.current = scope
|
|
156
156
|
|
|
157
157
|
return resource
|
|
158
158
|
}
|
|
@@ -161,8 +161,7 @@ export const useRcResource = <T>(
|
|
|
161
161
|
// we are using this cache to avoid starting multiple queries/spans for the same component.
|
|
162
162
|
// This is somewhat against some recommended React best practices, but it should be fine in our case below.
|
|
163
163
|
// Please definitely open an issue if you see or run into any problems with this approach!
|
|
164
|
-
|
|
165
|
-
string,
|
|
164
|
+
type Entry =
|
|
166
165
|
| {
|
|
167
166
|
_tag: 'active'
|
|
168
167
|
rc: number
|
|
@@ -171,8 +170,23 @@ const cache = new Map<
|
|
|
171
170
|
| {
|
|
172
171
|
_tag: 'destroyed'
|
|
173
172
|
}
|
|
174
|
-
|
|
173
|
+
|
|
174
|
+
type Bucket = Map<string, Entry>
|
|
175
|
+
|
|
176
|
+
// Per-scope buckets. Keying by the scope object (e.g. a Store instance) ensures that
|
|
177
|
+
// when the scope is replaced (store dispose/recreate), the new scope gets a fresh bucket
|
|
178
|
+
// and stale entries from the disposed scope become GC-eligible. See issue #1186.
|
|
179
|
+
let scopedBuckets = new WeakMap<object, Bucket>()
|
|
180
|
+
|
|
181
|
+
const getBucket = (scope: object): Bucket => {
|
|
182
|
+
let bucket = scopedBuckets.get(scope)
|
|
183
|
+
if (bucket === undefined) {
|
|
184
|
+
bucket = new Map()
|
|
185
|
+
scopedBuckets.set(scope, bucket)
|
|
186
|
+
}
|
|
187
|
+
return bucket
|
|
188
|
+
}
|
|
175
189
|
|
|
176
190
|
export const __resetUseRcResourceCache = () => {
|
|
177
|
-
|
|
191
|
+
scopedBuckets = new WeakMap()
|
|
178
192
|
}
|