@isograph/react-disposable-state 0.1.1 → 0.2.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/dist/ParentCache.js +2 -0
- package/dist/useCachedPrecommitValue.d.ts +2 -2
- package/dist/useCachedPrecommitValue.js +1 -1
- package/package.json +9 -5
- package/src/ParentCache.ts +2 -0
- package/src/useCachedPrecommitValue.test.tsx +414 -420
- package/src/useCachedPrecommitValue.ts +2 -2
- package/src/useUpdatableDisposableState.ts +1 -1
- package/tsconfig.json +6 -0
package/dist/ParentCache.js
CHANGED
@@ -21,6 +21,8 @@ const CacheItem_1 = require("./CacheItem");
|
|
21
21
|
* be called
|
22
22
|
*/
|
23
23
|
class ParentCache {
|
24
|
+
// TODO pass an onEmpty function, which can e.g. remove this ParentCache
|
25
|
+
// from some parent object.
|
24
26
|
constructor(factory) {
|
25
27
|
this.__cacheItem = null;
|
26
28
|
this.__factory = factory;
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { ParentCache } from './ParentCache';
|
2
|
-
import { ItemCleanupPair } from '@isograph/
|
2
|
+
import { ItemCleanupPair } from '@isograph/disposable-types';
|
3
3
|
/**
|
4
4
|
* usePrecommitValue<T>
|
5
5
|
* - Takes a mutable parent cache, a factory function, and an onCommit callback.
|
@@ -9,7 +9,7 @@ import { ItemCleanupPair } from '@isograph/isograph-disposable-types/dist';
|
|
9
9
|
* disposed by the time of the commit. If so, this hook checks the parent cache
|
10
10
|
* for another T or creates one, and passes this T to onCommit.
|
11
11
|
* - If the T returned during the last render is not the same as the one that
|
12
|
-
* is passed to onCommit, during the commit phase, will schedule another render.
|
12
|
+
* is passed to onCommit, during the commit phase, it will schedule another render.
|
13
13
|
*
|
14
14
|
* Invariant: the returned T has not been disposed during the tick of the render.
|
15
15
|
* The T passed to the onCommit callback has not been disposed when the onCommit
|
@@ -12,7 +12,7 @@ const useHasCommittedRef_1 = require("./useHasCommittedRef");
|
|
12
12
|
* disposed by the time of the commit. If so, this hook checks the parent cache
|
13
13
|
* for another T or creates one, and passes this T to onCommit.
|
14
14
|
* - If the T returned during the last render is not the same as the one that
|
15
|
-
* is passed to onCommit, during the commit phase, will schedule another render.
|
15
|
+
* is passed to onCommit, during the commit phase, it will schedule another render.
|
16
16
|
*
|
17
17
|
* Invariant: the returned T has not been disposed during the tick of the render.
|
18
18
|
* The T passed to the onCommit callback has not been disposed when the onCommit
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@isograph/react-disposable-state",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.2.0",
|
4
4
|
"description": "Primitives for managing disposable state in React",
|
5
5
|
"homepage": "https://isograph.dev",
|
6
6
|
"main": "dist/index.js",
|
@@ -14,11 +14,14 @@
|
|
14
14
|
"test-watch": "vitest watch",
|
15
15
|
"coverage": "vitest run --coverage",
|
16
16
|
"note": "WE SHOULD ALSO TEST HERE",
|
17
|
-
"prepack": "yarn run compile"
|
17
|
+
"prepack": "yarn run compile",
|
18
|
+
"tsc": "tsc"
|
18
19
|
},
|
19
20
|
"dependencies": {
|
20
|
-
"@isograph/disposable-types": "0.
|
21
|
-
|
21
|
+
"@isograph/disposable-types": "0.2.0"
|
22
|
+
},
|
23
|
+
"peerDependencies": {
|
24
|
+
"react": "18.2.0"
|
22
25
|
},
|
23
26
|
"devDependencies": {
|
24
27
|
"@types/react": "^18.0.31",
|
@@ -30,5 +33,6 @@
|
|
30
33
|
"type": "git",
|
31
34
|
"url": "git+https://github.com/isographlabs/isograph.git",
|
32
35
|
"directory": "libs/isograph-react-disposable-state"
|
33
|
-
}
|
36
|
+
},
|
37
|
+
"sideEffects": false
|
34
38
|
}
|
package/src/ParentCache.ts
CHANGED
@@ -28,6 +28,8 @@ export class ParentCache<T> {
|
|
28
28
|
private __cacheItem: CacheItem<T> | null = null;
|
29
29
|
private readonly __factory: Factory<T>;
|
30
30
|
|
31
|
+
// TODO pass an onEmpty function, which can e.g. remove this ParentCache
|
32
|
+
// from some parent object.
|
31
33
|
constructor(factory: Factory<T>) {
|
32
34
|
this.__factory = factory;
|
33
35
|
}
|
@@ -60,13 +60,135 @@ async function awaitableCreate(Component, isConcurrent) {
|
|
60
60
|
return element;
|
61
61
|
}
|
62
62
|
|
63
|
-
describe('
|
64
|
-
test('
|
65
|
-
|
63
|
+
describe('useCachedPrecommitValue', () => {
|
64
|
+
test('on initial render, it should call getOrPopulateAndTemporaryRetain', async () => {
|
65
|
+
const disposeItem = vi.fn();
|
66
|
+
const factory = vi.fn(() => {
|
67
|
+
const pair: ItemCleanupPair<number> = [1, disposeItem];
|
68
|
+
return pair;
|
69
|
+
});
|
70
|
+
const cache = new ParentCache(factory);
|
71
|
+
const getOrPopulateAndTemporaryRetain = vi.spyOn(
|
72
|
+
cache,
|
73
|
+
'getOrPopulateAndTemporaryRetain',
|
74
|
+
);
|
75
|
+
|
76
|
+
const componentCommits = vi.fn();
|
77
|
+
const hookOnCommit = vi.fn();
|
78
|
+
const render = vi.fn();
|
79
|
+
function TestComponent() {
|
80
|
+
render();
|
81
|
+
React.useEffect(componentCommits);
|
82
|
+
|
83
|
+
const data = useCachedPrecommitValue(cache, hookOnCommit);
|
84
|
+
|
85
|
+
expect(render).toBeCalledTimes(1);
|
86
|
+
expect(componentCommits).not.toBeCalled();
|
87
|
+
expect(hookOnCommit).not.toBeCalled();
|
88
|
+
expect(data).toEqual({ state: 1 });
|
89
|
+
expect(factory).toBeCalledTimes(1);
|
90
|
+
expect(disposeItem).not.toBeCalled();
|
91
|
+
expect(getOrPopulateAndTemporaryRetain).toBeCalledTimes(1);
|
92
|
+
|
93
|
+
// TODO we should assert that permanentRetainIfNotDisposed was called
|
94
|
+
// on the cache item.
|
95
|
+
|
96
|
+
return <div />;
|
97
|
+
}
|
98
|
+
|
99
|
+
await awaitableCreate(<TestComponent />, false);
|
100
|
+
|
101
|
+
expect(componentCommits).toBeCalledTimes(1);
|
102
|
+
expect(hookOnCommit).toBeCalledTimes(1);
|
103
|
+
expect(render).toHaveBeenCalledTimes(1);
|
104
|
+
});
|
105
|
+
|
106
|
+
test('on commit, it should call the provided callback and empty the parent cache', async () => {
|
107
|
+
const disposeItem = vi.fn();
|
108
|
+
const factory = vi.fn(() => {
|
109
|
+
const pair: ItemCleanupPair<number> = [1, disposeItem];
|
110
|
+
return pair;
|
111
|
+
});
|
112
|
+
const cache = new ParentCache(factory);
|
113
|
+
|
114
|
+
const componentCommits = vi.fn();
|
115
|
+
const hookOnCommit = vi.fn();
|
116
|
+
const render = vi.fn();
|
117
|
+
function TestComponent() {
|
118
|
+
render();
|
119
|
+
expect(render).toHaveBeenCalledTimes(1);
|
120
|
+
const data = useCachedPrecommitValue(cache, hookOnCommit);
|
121
|
+
|
122
|
+
React.useEffect(() => {
|
123
|
+
componentCommits();
|
124
|
+
expect(componentCommits).toHaveBeenCalledTimes(1);
|
125
|
+
expect(hookOnCommit).toBeCalledTimes(1);
|
126
|
+
expect(hookOnCommit.mock.calls[0][0][0]).toBe(1);
|
127
|
+
expect(typeof hookOnCommit.mock.calls[0][0][1]).toBe('function');
|
128
|
+
expect(factory).toBeCalledTimes(1);
|
129
|
+
expect(disposeItem).not.toBeCalled();
|
130
|
+
expect(cache.isEmpty()).toBe(true);
|
131
|
+
}, []);
|
132
|
+
|
133
|
+
expect(factory).toBeCalledTimes(1);
|
134
|
+
expect(disposeItem).not.toBeCalled();
|
135
|
+
return <div />;
|
136
|
+
}
|
137
|
+
|
138
|
+
await awaitableCreate(<TestComponent />, false);
|
139
|
+
expect(componentCommits).toBeCalledTimes(1);
|
140
|
+
expect(render).toHaveBeenCalledTimes(1);
|
141
|
+
});
|
142
|
+
|
143
|
+
test('after commit, on subsequent renders it should return null', async () => {
|
144
|
+
const disposeItem = vi.fn();
|
145
|
+
const factory = vi.fn(() => {
|
146
|
+
const pair: ItemCleanupPair<number> = [1, disposeItem];
|
147
|
+
return pair;
|
148
|
+
});
|
149
|
+
const cache = new ParentCache(factory);
|
150
|
+
|
151
|
+
const componentCommits = vi.fn();
|
152
|
+
const hookOnCommit = vi.fn();
|
153
|
+
let setState;
|
154
|
+
let initialRender = true;
|
155
|
+
function TestComponent() {
|
156
|
+
const [, _setState] = React.useState(null);
|
157
|
+
setState = _setState;
|
158
|
+
const value = useCachedPrecommitValue(cache, hookOnCommit);
|
159
|
+
|
160
|
+
if (initialRender && value !== null) {
|
161
|
+
initialRender = false;
|
162
|
+
expect(value).toEqual({ state: 1 });
|
163
|
+
} else {
|
164
|
+
expect(value).toEqual(null);
|
165
|
+
}
|
66
166
|
|
67
|
-
|
68
|
-
|
69
|
-
|
167
|
+
React.useEffect(() => {
|
168
|
+
componentCommits();
|
169
|
+
expect(componentCommits).toHaveBeenCalledTimes(1);
|
170
|
+
expect(hookOnCommit).toBeCalledTimes(1);
|
171
|
+
expect(factory).toBeCalledTimes(1);
|
172
|
+
expect(disposeItem).not.toBeCalled();
|
173
|
+
}, []);
|
174
|
+
|
175
|
+
return <div />;
|
176
|
+
}
|
177
|
+
|
178
|
+
await awaitableCreate(<TestComponent />, false);
|
179
|
+
|
180
|
+
expect(componentCommits).toHaveBeenCalledTimes(1);
|
181
|
+
|
182
|
+
// Trigger a re-render
|
183
|
+
setState({});
|
184
|
+
await shortPromise();
|
185
|
+
expect(initialRender).toBe(false);
|
186
|
+
});
|
187
|
+
|
188
|
+
test(
|
189
|
+
'on repeated pre-commit renders, if the temporary retain is not disposed, ' +
|
190
|
+
'it should re-call getOrPopulateAndTemporaryRetain but not call factory again',
|
191
|
+
async () => {
|
70
192
|
const disposeItem = vi.fn();
|
71
193
|
const factory = vi.fn(() => {
|
72
194
|
const pair: ItemCleanupPair<number> = [1, disposeItem];
|
@@ -81,497 +203,369 @@ if (false) {
|
|
81
203
|
const componentCommits = vi.fn();
|
82
204
|
const hookOnCommit = vi.fn();
|
83
205
|
const render = vi.fn();
|
206
|
+
let renderCount = 0;
|
84
207
|
function TestComponent() {
|
85
208
|
render();
|
86
|
-
|
209
|
+
const value = useCachedPrecommitValue(cache, hookOnCommit);
|
87
210
|
|
88
|
-
|
211
|
+
expect(value).toEqual({ state: 1 });
|
212
|
+
expect(factory).toHaveBeenCalledTimes(1);
|
89
213
|
|
90
|
-
|
91
|
-
expect(
|
92
|
-
|
93
|
-
|
94
|
-
expect(factory).toBeCalledTimes(1);
|
95
|
-
expect(disposeItem).not.toBeCalled();
|
96
|
-
expect(getOrPopulateAndTemporaryRetain).toBeCalledTimes(1);
|
214
|
+
renderCount++;
|
215
|
+
expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(
|
216
|
+
renderCount,
|
217
|
+
);
|
97
218
|
|
98
|
-
|
99
|
-
|
219
|
+
React.useEffect(() => {
|
220
|
+
componentCommits();
|
221
|
+
expect(componentCommits).toHaveBeenCalledTimes(1);
|
222
|
+
expect(hookOnCommit).toBeCalledTimes(1);
|
223
|
+
expect(factory).toBeCalledTimes(1);
|
224
|
+
expect(disposeItem).not.toBeCalled();
|
225
|
+
}, []);
|
100
226
|
|
101
227
|
return <div />;
|
102
228
|
}
|
103
229
|
|
104
|
-
|
230
|
+
const { promise, isResolvedRef, resolve } = promiseAndResolver();
|
231
|
+
await awaitableCreate(
|
232
|
+
<React.Suspense fallback={<div />}>
|
233
|
+
<TestComponent />
|
234
|
+
<Suspender promise={promise} isResolvedRef={isResolvedRef} />
|
235
|
+
</React.Suspense>,
|
236
|
+
true,
|
237
|
+
);
|
105
238
|
|
106
|
-
expect(componentCommits).
|
107
|
-
expect(hookOnCommit).toBeCalledTimes(1);
|
239
|
+
expect(componentCommits).toHaveBeenCalledTimes(0);
|
108
240
|
expect(render).toHaveBeenCalledTimes(1);
|
109
|
-
});
|
110
241
|
|
111
|
-
|
242
|
+
resolve();
|
243
|
+
await shortPromise();
|
244
|
+
|
245
|
+
expect(componentCommits).toHaveBeenCalledTimes(1);
|
246
|
+
expect(render).toHaveBeenCalledTimes(2);
|
247
|
+
},
|
248
|
+
);
|
249
|
+
|
250
|
+
test(
|
251
|
+
'on repeated pre-commit renders, if the temporary retain is disposed, ' +
|
252
|
+
'it should re-call getOrPopulateAndTemporaryRetain and factory',
|
253
|
+
async () => {
|
112
254
|
const disposeItem = vi.fn();
|
255
|
+
let factoryValue = 0;
|
113
256
|
const factory = vi.fn(() => {
|
114
|
-
|
257
|
+
factoryValue++;
|
258
|
+
const pair: ItemCleanupPair<number> = [factoryValue, disposeItem];
|
115
259
|
return pair;
|
116
260
|
});
|
117
261
|
const cache = new ParentCache(factory);
|
118
262
|
|
263
|
+
const getOrPopulateAndTemporaryRetain = vi.spyOn(
|
264
|
+
cache,
|
265
|
+
'getOrPopulateAndTemporaryRetain',
|
266
|
+
);
|
267
|
+
|
119
268
|
const componentCommits = vi.fn();
|
120
269
|
const hookOnCommit = vi.fn();
|
121
270
|
const render = vi.fn();
|
122
271
|
function TestComponent() {
|
123
272
|
render();
|
124
|
-
|
125
|
-
|
273
|
+
const value = useCachedPrecommitValue(cache, hookOnCommit);
|
274
|
+
|
275
|
+
expect(value).toEqual({ state: factoryValue });
|
126
276
|
|
127
277
|
React.useEffect(() => {
|
128
278
|
componentCommits();
|
279
|
+
expect(cache.isEmpty()).toBe(true);
|
129
280
|
expect(componentCommits).toHaveBeenCalledTimes(1);
|
130
281
|
expect(hookOnCommit).toBeCalledTimes(1);
|
131
|
-
expect(hookOnCommit.mock.calls[0][0][0]).toBe(
|
132
|
-
expect(
|
133
|
-
expect(
|
134
|
-
expect(disposeItem).not.toBeCalled();
|
135
|
-
expect(cache.isEmpty()).toBe(true);
|
282
|
+
expect(hookOnCommit.mock.calls[0][0][0]).toBe(2);
|
283
|
+
expect(factory).toBeCalledTimes(2);
|
284
|
+
expect(disposeItem).toBeCalledTimes(1);
|
136
285
|
}, []);
|
137
286
|
|
138
|
-
|
139
|
-
|
287
|
+
if (render.mock.calls.length === 1) {
|
288
|
+
expect(factory).toHaveBeenCalledTimes(1);
|
289
|
+
// First render, dispose the temporary retain
|
290
|
+
expect(disposeItem).toBeCalledTimes(0);
|
291
|
+
getOrPopulateAndTemporaryRetain.mock.results[0].value[2]();
|
292
|
+
expect(disposeItem).toBeCalledTimes(1);
|
293
|
+
expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(1);
|
294
|
+
} else {
|
295
|
+
expect(factory).toHaveBeenCalledTimes(2);
|
296
|
+
expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(2);
|
297
|
+
}
|
298
|
+
|
140
299
|
return <div />;
|
141
300
|
}
|
142
301
|
|
143
|
-
|
144
|
-
|
302
|
+
const { promise, isResolvedRef, resolve } = promiseAndResolver();
|
303
|
+
await awaitableCreate(
|
304
|
+
<React.Suspense fallback={<div />}>
|
305
|
+
<TestComponent />
|
306
|
+
<Suspender promise={promise} isResolvedRef={isResolvedRef} />
|
307
|
+
</React.Suspense>,
|
308
|
+
true,
|
309
|
+
);
|
310
|
+
|
311
|
+
expect(componentCommits).toHaveBeenCalledTimes(0);
|
145
312
|
expect(render).toHaveBeenCalledTimes(1);
|
146
|
-
});
|
147
313
|
|
148
|
-
|
314
|
+
resolve();
|
315
|
+
await shortPromise();
|
316
|
+
|
317
|
+
expect(componentCommits).toHaveBeenCalledTimes(1);
|
318
|
+
expect(render).toHaveBeenCalledTimes(2);
|
319
|
+
},
|
320
|
+
);
|
321
|
+
|
322
|
+
test(
|
323
|
+
'if the item has been disposed between the render and the commit, ' +
|
324
|
+
'and the parent cache is empty, it will call factory again, re-render an ' +
|
325
|
+
'additional time and called onCommit with the newly generated item',
|
326
|
+
async () => {
|
149
327
|
const disposeItem = vi.fn();
|
328
|
+
let factoryCount = 0;
|
150
329
|
const factory = vi.fn(() => {
|
151
|
-
|
330
|
+
factoryCount++;
|
331
|
+
const pair: ItemCleanupPair<number> = [factoryCount, disposeItem];
|
152
332
|
return pair;
|
153
333
|
});
|
154
334
|
const cache = new ParentCache(factory);
|
335
|
+
const getOrPopulateAndTemporaryRetain = vi.spyOn(
|
336
|
+
cache,
|
337
|
+
'getOrPopulateAndTemporaryRetain',
|
338
|
+
);
|
339
|
+
const getAndPermanentRetainIfPresent = vi.spyOn(
|
340
|
+
cache,
|
341
|
+
'getAndPermanentRetainIfPresent',
|
342
|
+
);
|
155
343
|
|
156
344
|
const componentCommits = vi.fn();
|
157
345
|
const hookOnCommit = vi.fn();
|
158
|
-
|
159
|
-
let initialRender = true;
|
346
|
+
const render = vi.fn();
|
160
347
|
function TestComponent() {
|
161
|
-
|
162
|
-
setState = _setState;
|
163
|
-
const value = useCachedPrecommitValue(cache, hookOnCommit);
|
348
|
+
render();
|
164
349
|
|
165
|
-
|
166
|
-
initialRender = false;
|
167
|
-
expect(value).toEqual({ state: 1 });
|
168
|
-
} else {
|
169
|
-
expect(value).toEqual(null);
|
170
|
-
}
|
350
|
+
useCachedPrecommitValue(cache, hookOnCommit);
|
171
351
|
|
172
352
|
React.useEffect(() => {
|
173
353
|
componentCommits();
|
174
|
-
expect(
|
175
|
-
expect(
|
176
|
-
expect(
|
177
|
-
|
354
|
+
expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(1);
|
355
|
+
expect(getAndPermanentRetainIfPresent).toHaveBeenCalledTimes(1);
|
356
|
+
expect(getAndPermanentRetainIfPresent.mock.results[0].value).toBe(
|
357
|
+
null,
|
358
|
+
);
|
359
|
+
expect(factory).toHaveBeenCalledTimes(2);
|
360
|
+
expect(cache.isEmpty()).toBe(true);
|
361
|
+
expect(hookOnCommit).toHaveBeenCalledTimes(1);
|
362
|
+
expect(hookOnCommit.mock.calls[0][0][0]).toBe(2);
|
178
363
|
}, []);
|
179
364
|
|
180
365
|
return <div />;
|
181
366
|
}
|
182
367
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
//
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
const getOrPopulateAndTemporaryRetain = vi.spyOn(
|
204
|
-
cache,
|
205
|
-
'getOrPopulateAndTemporaryRetain',
|
206
|
-
);
|
207
|
-
|
208
|
-
const componentCommits = vi.fn();
|
209
|
-
const hookOnCommit = vi.fn();
|
210
|
-
const render = vi.fn();
|
211
|
-
let renderCount = 0;
|
212
|
-
function TestComponent() {
|
213
|
-
render();
|
214
|
-
const value = useCachedPrecommitValue(cache, hookOnCommit);
|
368
|
+
// wat is going on?
|
369
|
+
//
|
370
|
+
// We want to test a scenario where the item is disposed between the render and
|
371
|
+
// the commit.
|
372
|
+
//
|
373
|
+
// The subcomponents are rendered in order: TestComponent followed by CodeExecutor.
|
374
|
+
//
|
375
|
+
// - During TestComponent's render, it will populate the cache.
|
376
|
+
// - Then, CodeExecutor will render, and dispose the temporary retain,
|
377
|
+
// disposing the cache item. The parent cache will be empty as well.
|
378
|
+
// - Then, TestComponent commits.
|
379
|
+
let initialRender = true;
|
380
|
+
function CodeExecutor() {
|
381
|
+
if (initialRender) {
|
382
|
+
// This code executes after the initial render of TestComponent, but before
|
383
|
+
// it commits.
|
384
|
+
expect(disposeItem).not.toHaveBeenCalled();
|
385
|
+
getOrPopulateAndTemporaryRetain.mock.results[0].value[2]();
|
386
|
+
expect(disposeItem).toHaveBeenCalledTimes(1);
|
387
|
+
expect(cache.isEmpty()).toBe(true);
|
215
388
|
|
216
|
-
expect(
|
389
|
+
expect(render).toHaveBeenCalledTimes(1);
|
390
|
+
expect(hookOnCommit).toBeCalledTimes(0);
|
391
|
+
expect(componentCommits).toBeCalledTimes(0);
|
217
392
|
expect(factory).toHaveBeenCalledTimes(1);
|
218
393
|
|
219
|
-
|
220
|
-
expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(
|
221
|
-
renderCount,
|
222
|
-
);
|
223
|
-
|
224
|
-
React.useEffect(() => {
|
225
|
-
componentCommits();
|
226
|
-
expect(componentCommits).toHaveBeenCalledTimes(1);
|
227
|
-
expect(hookOnCommit).toBeCalledTimes(1);
|
228
|
-
expect(factory).toBeCalledTimes(1);
|
229
|
-
expect(disposeItem).not.toBeCalled();
|
230
|
-
}, []);
|
231
|
-
|
232
|
-
return <div />;
|
394
|
+
initialRender = false;
|
233
395
|
}
|
234
396
|
|
235
|
-
|
236
|
-
|
237
|
-
<React.Suspense fallback={<div />}>
|
238
|
-
<TestComponent />
|
239
|
-
<Suspender promise={promise} isResolvedRef={isResolvedRef} />
|
240
|
-
</React.Suspense>,
|
241
|
-
true,
|
242
|
-
);
|
243
|
-
|
244
|
-
expect(componentCommits).toHaveBeenCalledTimes(0);
|
245
|
-
expect(render).toHaveBeenCalledTimes(1);
|
397
|
+
return null;
|
398
|
+
}
|
246
399
|
|
247
|
-
|
248
|
-
|
400
|
+
const element = await awaitableCreate(
|
401
|
+
<>
|
402
|
+
<TestComponent />
|
403
|
+
<CodeExecutor />
|
404
|
+
</>,
|
405
|
+
false,
|
406
|
+
);
|
249
407
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
408
|
+
// This code executes after the commit and re-render of TestComponent.
|
409
|
+
// The commit triggers a re-render, because the item was disposed.
|
410
|
+
expect(render).toHaveBeenCalledTimes(2);
|
411
|
+
expect(factory).toBeCalledTimes(2);
|
412
|
+
},
|
413
|
+
);
|
254
414
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
415
|
+
test(
|
416
|
+
'if, between the render and the commit, the item has been disposed, ' +
|
417
|
+
'and the parent cache is not empty, it will not call factory again, will re-render ' +
|
418
|
+
'an additional time and will call onCommit with the value in the parent cache',
|
419
|
+
async () => {
|
420
|
+
const disposeItem = vi.fn();
|
421
|
+
let factoryCount = 0;
|
422
|
+
const factory = vi.fn(() => {
|
423
|
+
factoryCount++;
|
424
|
+
const pair: ItemCleanupPair<number> = [factoryCount, disposeItem];
|
425
|
+
return pair;
|
426
|
+
});
|
427
|
+
const cache = new ParentCache(factory);
|
428
|
+
const getAndPermanentRetainIfPresent = vi.spyOn(
|
429
|
+
cache,
|
430
|
+
'getAndPermanentRetainIfPresent',
|
431
|
+
);
|
272
432
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
expect(value).toEqual({ state: factoryValue });
|
281
|
-
|
282
|
-
React.useEffect(() => {
|
283
|
-
componentCommits();
|
284
|
-
expect(cache.isEmpty()).toBe(true);
|
285
|
-
expect(componentCommits).toHaveBeenCalledTimes(1);
|
286
|
-
expect(hookOnCommit).toBeCalledTimes(1);
|
287
|
-
expect(hookOnCommit.mock.calls[0][0][0]).toBe(2);
|
288
|
-
expect(factory).toBeCalledTimes(2);
|
289
|
-
expect(disposeItem).toBeCalledTimes(1);
|
290
|
-
}, []);
|
291
|
-
|
292
|
-
if (render.mock.calls.length === 1) {
|
293
|
-
expect(factory).toHaveBeenCalledTimes(1);
|
294
|
-
// First render, dispose the temporary retain
|
295
|
-
expect(disposeItem).toBeCalledTimes(0);
|
296
|
-
getOrPopulateAndTemporaryRetain.mock.results[0].value[2]();
|
297
|
-
expect(disposeItem).toBeCalledTimes(1);
|
298
|
-
expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(1);
|
299
|
-
} else {
|
300
|
-
expect(factory).toHaveBeenCalledTimes(2);
|
301
|
-
expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(2);
|
302
|
-
}
|
303
|
-
|
304
|
-
return <div />;
|
305
|
-
}
|
433
|
+
const componentCommits = vi.fn();
|
434
|
+
const hookOnCommit = vi.fn();
|
435
|
+
const render = vi.fn();
|
436
|
+
function TestComponent() {
|
437
|
+
render();
|
438
|
+
useCachedPrecommitValue(cache, hookOnCommit);
|
306
439
|
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
440
|
+
React.useEffect(() => {
|
441
|
+
componentCommits();
|
442
|
+
// Note that we called getOrPopulateAndTemporaryRetain during CodeExecutor, hence 2
|
443
|
+
expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(2);
|
444
|
+
expect(getAndPermanentRetainIfPresent).toHaveBeenCalledTimes(1);
|
445
|
+
expect(getAndPermanentRetainIfPresent.mock.results[0].value[0]).toBe(
|
446
|
+
2,
|
447
|
+
);
|
448
|
+
expect(factory).toHaveBeenCalledTimes(2);
|
449
|
+
expect(hookOnCommit).toHaveBeenCalledTimes(1);
|
450
|
+
expect(hookOnCommit.mock.calls[0][0][0]).toBe(2);
|
451
|
+
}, []);
|
315
452
|
|
316
|
-
|
317
|
-
|
453
|
+
return <div />;
|
454
|
+
}
|
318
455
|
|
319
|
-
|
320
|
-
|
456
|
+
const getOrPopulateAndTemporaryRetain = vi.spyOn(
|
457
|
+
cache,
|
458
|
+
'getOrPopulateAndTemporaryRetain',
|
459
|
+
);
|
321
460
|
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
461
|
+
// wat is going on?
|
462
|
+
//
|
463
|
+
// We want to test a scenario where the item is disposed between the render and
|
464
|
+
// the commit.
|
465
|
+
//
|
466
|
+
// The subcomponents are rendered in order: TestComponent followed by CodeExecutor.
|
467
|
+
//
|
468
|
+
// - During TestComponent's render, it will populate the cache.
|
469
|
+
// - Then, CodeExecutor will render, and dispose the temporary retain,
|
470
|
+
// disposing the cache item. It will then repopulate the parent cache.
|
471
|
+
// - Then, TestComponent commits.
|
472
|
+
let initialRender = true;
|
473
|
+
function CodeExecutor() {
|
474
|
+
if (initialRender) {
|
475
|
+
// This code executes after the initial render of TestComponent, but before
|
476
|
+
// it commits.
|
477
|
+
expect(disposeItem).not.toHaveBeenCalled();
|
478
|
+
getOrPopulateAndTemporaryRetain.mock.results[0].value[2]();
|
479
|
+
expect(disposeItem).toHaveBeenCalledTimes(1);
|
480
|
+
expect(cache.isEmpty()).toBe(true);
|
326
481
|
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
async () => {
|
332
|
-
const disposeItem = vi.fn();
|
333
|
-
let factoryCount = 0;
|
334
|
-
const factory = vi.fn(() => {
|
335
|
-
factoryCount++;
|
336
|
-
const pair: ItemCleanupPair<number> = [factoryCount, disposeItem];
|
337
|
-
return pair;
|
338
|
-
});
|
339
|
-
const cache = new ParentCache(factory);
|
340
|
-
const getOrPopulateAndTemporaryRetain = vi.spyOn(
|
341
|
-
cache,
|
342
|
-
'getOrPopulateAndTemporaryRetain',
|
343
|
-
);
|
344
|
-
const getAndPermanentRetainIfPresent = vi.spyOn(
|
345
|
-
cache,
|
346
|
-
'getAndPermanentRetainIfPresent',
|
347
|
-
);
|
482
|
+
cache.getOrPopulateAndTemporaryRetain();
|
483
|
+
expect(cache.isEmpty()).toBe(false);
|
484
|
+
// The factory function was called when we called getOrPopulateAndTemporaryRetain
|
485
|
+
expect(factory).toHaveBeenCalledTimes(2);
|
348
486
|
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
function TestComponent() {
|
353
|
-
render();
|
354
|
-
|
355
|
-
useCachedPrecommitValue(cache, hookOnCommit);
|
356
|
-
|
357
|
-
React.useEffect(() => {
|
358
|
-
componentCommits();
|
359
|
-
expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(1);
|
360
|
-
expect(getAndPermanentRetainIfPresent).toHaveBeenCalledTimes(1);
|
361
|
-
expect(getAndPermanentRetainIfPresent.mock.results[0].value).toBe(
|
362
|
-
null,
|
363
|
-
);
|
364
|
-
expect(factory).toHaveBeenCalledTimes(2);
|
365
|
-
expect(cache.isEmpty()).toBe(true);
|
366
|
-
expect(hookOnCommit).toHaveBeenCalledTimes(1);
|
367
|
-
expect(hookOnCommit.mock.calls[0][0][0]).toBe(2);
|
368
|
-
}, []);
|
369
|
-
|
370
|
-
return <div />;
|
371
|
-
}
|
487
|
+
expect(render).toHaveBeenCalledTimes(1);
|
488
|
+
expect(hookOnCommit).toBeCalledTimes(0);
|
489
|
+
expect(componentCommits).toBeCalledTimes(0);
|
372
490
|
|
373
|
-
|
374
|
-
//
|
375
|
-
// We want to test a scenario where the item is disposed between the render and
|
376
|
-
// the commit.
|
377
|
-
//
|
378
|
-
// The subcomponents are rendered in order: TestComponent followed by CodeExecutor.
|
379
|
-
//
|
380
|
-
// - During TestComponent's render, it will populate the cache.
|
381
|
-
// - Then, CodeExecutor will render, and dispose the temporary retain,
|
382
|
-
// disposing the cache item. The parent cache will be empty as well.
|
383
|
-
// - Then, TestComponent commits.
|
384
|
-
let initialRender = true;
|
385
|
-
function CodeExecutor() {
|
386
|
-
if (initialRender) {
|
387
|
-
// This code executes after the initial render of TestComponent, but before
|
388
|
-
// it commits.
|
389
|
-
expect(disposeItem).not.toHaveBeenCalled();
|
390
|
-
getOrPopulateAndTemporaryRetain.mock.results[0].value[2]();
|
391
|
-
expect(disposeItem).toHaveBeenCalledTimes(1);
|
392
|
-
expect(cache.isEmpty()).toBe(true);
|
393
|
-
|
394
|
-
expect(render).toHaveBeenCalledTimes(1);
|
395
|
-
expect(hookOnCommit).toBeCalledTimes(0);
|
396
|
-
expect(componentCommits).toBeCalledTimes(0);
|
397
|
-
expect(factory).toHaveBeenCalledTimes(1);
|
398
|
-
|
399
|
-
initialRender = false;
|
400
|
-
}
|
401
|
-
|
402
|
-
return null;
|
491
|
+
initialRender = false;
|
403
492
|
}
|
404
493
|
|
405
|
-
|
406
|
-
|
407
|
-
<TestComponent />
|
408
|
-
<CodeExecutor />
|
409
|
-
</>,
|
410
|
-
false,
|
411
|
-
);
|
412
|
-
|
413
|
-
// This code executes after the commit and re-render of TestComponent.
|
414
|
-
// The commit triggers a re-render, because the item was disposed.
|
415
|
-
expect(render).toHaveBeenCalledTimes(2);
|
416
|
-
expect(factory).toBeCalledTimes(2);
|
417
|
-
},
|
418
|
-
);
|
494
|
+
return null;
|
495
|
+
}
|
419
496
|
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
const factory = vi.fn(() => {
|
428
|
-
factoryCount++;
|
429
|
-
const pair: ItemCleanupPair<number> = [factoryCount, disposeItem];
|
430
|
-
return pair;
|
431
|
-
});
|
432
|
-
const cache = new ParentCache(factory);
|
433
|
-
const getAndPermanentRetainIfPresent = vi.spyOn(
|
434
|
-
cache,
|
435
|
-
'getAndPermanentRetainIfPresent',
|
436
|
-
);
|
497
|
+
const element = await awaitableCreate(
|
498
|
+
<React.Suspense fallback="fallback">
|
499
|
+
<TestComponent />
|
500
|
+
<CodeExecutor />
|
501
|
+
</React.Suspense>,
|
502
|
+
false,
|
503
|
+
);
|
437
504
|
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
componentCommits();
|
447
|
-
// Note that we called getOrPopulateAndTemporaryRetain during CodeExecutor, hence 2
|
448
|
-
expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(2);
|
449
|
-
expect(getAndPermanentRetainIfPresent).toHaveBeenCalledTimes(1);
|
450
|
-
expect(
|
451
|
-
getAndPermanentRetainIfPresent.mock.results[0].value[0],
|
452
|
-
).toBe(2);
|
453
|
-
expect(factory).toHaveBeenCalledTimes(2);
|
454
|
-
expect(hookOnCommit).toHaveBeenCalledTimes(1);
|
455
|
-
expect(hookOnCommit.mock.calls[0][0][0]).toBe(2);
|
456
|
-
}, []);
|
457
|
-
|
458
|
-
return <div />;
|
459
|
-
}
|
505
|
+
// This code executes after the commit and re-render of TestComponent.
|
506
|
+
// The commit triggers a re-render, because the item was disposed.
|
507
|
+
expect(render).toHaveBeenCalledTimes(2);
|
508
|
+
// Note that this is the same number of calls as inside of CodeExecutor,
|
509
|
+
// implying that the factory function was not called again.
|
510
|
+
expect(factory).toBeCalledTimes(2);
|
511
|
+
},
|
512
|
+
);
|
460
513
|
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
514
|
+
test(
|
515
|
+
'After render but before commit, the item will ' +
|
516
|
+
'be in the parent cache, temporarily retained',
|
517
|
+
async () => {
|
518
|
+
const disposeItem = vi.fn();
|
519
|
+
const factory = vi.fn(() => {
|
520
|
+
const pair: ItemCleanupPair<number> = [1, disposeItem];
|
521
|
+
return pair;
|
522
|
+
});
|
523
|
+
const cache = new ParentCache(factory);
|
465
524
|
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
// The subcomponents are rendered in order: TestComponent followed by CodeExecutor.
|
472
|
-
//
|
473
|
-
// - During TestComponent's render, it will populate the cache.
|
474
|
-
// - Then, CodeExecutor will render, and dispose the temporary retain,
|
475
|
-
// disposing the cache item. It will then repopulate the parent cache.
|
476
|
-
// - Then, TestComponent commits.
|
477
|
-
let initialRender = true;
|
478
|
-
function CodeExecutor() {
|
479
|
-
if (initialRender) {
|
480
|
-
// This code executes after the initial render of TestComponent, but before
|
481
|
-
// it commits.
|
482
|
-
expect(disposeItem).not.toHaveBeenCalled();
|
483
|
-
getOrPopulateAndTemporaryRetain.mock.results[0].value[2]();
|
484
|
-
expect(disposeItem).toHaveBeenCalledTimes(1);
|
485
|
-
expect(cache.isEmpty()).toBe(true);
|
486
|
-
|
487
|
-
cache.getOrPopulateAndTemporaryRetain();
|
488
|
-
expect(cache.isEmpty()).toBe(false);
|
489
|
-
// The factory function was called when we called getOrPopulateAndTemporaryRetain
|
490
|
-
expect(factory).toHaveBeenCalledTimes(2);
|
491
|
-
|
492
|
-
expect(render).toHaveBeenCalledTimes(1);
|
493
|
-
expect(hookOnCommit).toBeCalledTimes(0);
|
494
|
-
expect(componentCommits).toBeCalledTimes(0);
|
495
|
-
|
496
|
-
initialRender = false;
|
497
|
-
}
|
498
|
-
|
499
|
-
return null;
|
500
|
-
}
|
525
|
+
const componentCommits = vi.fn();
|
526
|
+
const hookOnCommit = vi.fn();
|
527
|
+
const render = vi.fn();
|
528
|
+
function TestComponent() {
|
529
|
+
render();
|
501
530
|
|
502
|
-
|
503
|
-
<React.Suspense fallback="fallback">
|
504
|
-
<TestComponent />
|
505
|
-
<CodeExecutor />
|
506
|
-
</React.Suspense>,
|
507
|
-
false,
|
508
|
-
);
|
531
|
+
useCachedPrecommitValue(cache, hookOnCommit);
|
509
532
|
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
// Note that this is the same number of calls as inside of CodeExecutor,
|
514
|
-
// implying that the factory function was not called again.
|
515
|
-
expect(factory).toBeCalledTimes(2);
|
516
|
-
},
|
517
|
-
);
|
533
|
+
React.useEffect(() => {
|
534
|
+
componentCommits();
|
535
|
+
}, []);
|
518
536
|
|
519
|
-
|
520
|
-
|
521
|
-
'be in the parent cache, temporarily retained',
|
522
|
-
async () => {
|
523
|
-
const disposeItem = vi.fn();
|
524
|
-
const factory = vi.fn(() => {
|
525
|
-
const pair: ItemCleanupPair<number> = [1, disposeItem];
|
526
|
-
return pair;
|
527
|
-
});
|
528
|
-
const cache = new ParentCache(factory);
|
529
|
-
|
530
|
-
const componentCommits = vi.fn();
|
531
|
-
const hookOnCommit = vi.fn();
|
532
|
-
const render = vi.fn();
|
533
|
-
function TestComponent() {
|
534
|
-
render();
|
535
|
-
|
536
|
-
useCachedPrecommitValue(cache, hookOnCommit);
|
537
|
-
|
538
|
-
React.useEffect(() => {
|
539
|
-
componentCommits();
|
540
|
-
}, []);
|
541
|
-
|
542
|
-
return <div />;
|
543
|
-
}
|
537
|
+
return <div />;
|
538
|
+
}
|
544
539
|
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
540
|
+
// wat is going on?
|
541
|
+
//
|
542
|
+
// We want to test a scenario where the component unmounts before committing.
|
543
|
+
// However, we cannot distinguish between an unmount before commit and a
|
544
|
+
// render and a commit that hasn't happened yet.
|
545
|
+
//
|
546
|
+
// This can be simulated with suspense.
|
547
|
+
//
|
548
|
+
// This test and 'on initial render, it should call getOrPopulateAndTemporaryRetain'
|
549
|
+
// can be merged
|
550
|
+
|
551
|
+
const { promise, isResolvedRef } = promiseAndResolver();
|
552
|
+
const element = await awaitableCreate(
|
553
|
+
<React.Suspense fallback={null}>
|
554
|
+
<TestComponent />
|
555
|
+
<Suspender promise={promise} isResolvedRef={isResolvedRef} />
|
556
|
+
</React.Suspense>,
|
557
|
+
true,
|
558
|
+
);
|
564
559
|
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
}
|
560
|
+
// This code executes after the commit and re-render of TestComponent.
|
561
|
+
// The commit triggers a re-render, because the item was disposed.
|
562
|
+
expect(render).toHaveBeenCalledTimes(1);
|
563
|
+
expect(componentCommits).toHaveBeenCalledTimes(0);
|
564
|
+
const item = getItem(cache)!;
|
565
|
+
const state = getState(item);
|
566
|
+
assert(state.kind === 'InParentCacheAndNotDisposed');
|
567
|
+
expect(state.permanentRetainCount).toBe(0);
|
568
|
+
expect(state.temporaryRetainCount).toBe(1);
|
569
|
+
},
|
570
|
+
);
|
571
|
+
});
|
@@ -3,7 +3,7 @@
|
|
3
3
|
import { useEffect, useState } from 'react';
|
4
4
|
import { ParentCache } from './ParentCache';
|
5
5
|
import { useHasCommittedRef } from './useHasCommittedRef';
|
6
|
-
import { ItemCleanupPair } from '@isograph/
|
6
|
+
import { ItemCleanupPair } from '@isograph/disposable-types';
|
7
7
|
|
8
8
|
/**
|
9
9
|
* usePrecommitValue<T>
|
@@ -14,7 +14,7 @@ import { ItemCleanupPair } from '@isograph/isograph-disposable-types/dist';
|
|
14
14
|
* disposed by the time of the commit. If so, this hook checks the parent cache
|
15
15
|
* for another T or creates one, and passes this T to onCommit.
|
16
16
|
* - If the T returned during the last render is not the same as the one that
|
17
|
-
* is passed to onCommit, during the commit phase, will schedule another render.
|
17
|
+
* is passed to onCommit, during the commit phase, it will schedule another render.
|
18
18
|
*
|
19
19
|
* Invariant: the returned T has not been disposed during the tick of the render.
|
20
20
|
* The T passed to the onCommit callback has not been disposed when the onCommit
|
@@ -2,7 +2,7 @@ import { ItemCleanupPair } from '@isograph/disposable-types';
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
3
3
|
import { useHasCommittedRef } from './useHasCommittedRef';
|
4
4
|
|
5
|
-
export const UNASSIGNED_STATE = Symbol();
|
5
|
+
export const UNASSIGNED_STATE: unique symbol = Symbol();
|
6
6
|
export type UnassignedState = typeof UNASSIGNED_STATE;
|
7
7
|
|
8
8
|
type UseUpdatableDisposableStateReturnValue<T> = {
|