@isograph/react-disposable-state 0.1.1 → 0.3.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/CacheItem.d.ts +1 -0
- package/dist/CacheItem.d.ts.map +1 -0
- package/dist/CacheItem.js +2 -2
- package/dist/ParentCache.d.ts +2 -1
- package/dist/ParentCache.d.ts.map +1 -0
- package/dist/ParentCache.js +3 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -1
- package/dist/{useCachedPrecommitValue.d.ts → useCachedResponsivePrecommitValue.d.ts} +12 -9
- package/dist/useCachedResponsivePrecommitValue.d.ts.map +1 -0
- package/dist/{useCachedPrecommitValue.js → useCachedResponsivePrecommitValue.js} +17 -16
- package/dist/useDisposableState.d.ts +2 -1
- package/dist/useDisposableState.d.ts.map +1 -0
- package/dist/useDisposableState.js +7 -6
- package/dist/useHasCommittedRef.d.ts +1 -0
- package/dist/useHasCommittedRef.d.ts.map +1 -0
- package/dist/useHasCommittedRef.js +1 -2
- package/dist/useLazyDisposableState.d.ts +4 -2
- package/dist/useLazyDisposableState.d.ts.map +1 -0
- package/dist/useLazyDisposableState.js +15 -12
- package/dist/useUpdatableDisposableState.d.ts +1 -0
- package/dist/useUpdatableDisposableState.d.ts.map +1 -0
- package/dist/useUpdatableDisposableState.js +2 -2
- package/package.json +11 -8
- package/src/ParentCache.ts +3 -1
- package/src/index.ts +1 -1
- package/src/useCachedResponsivePrecommitValue.test.tsx +571 -0
- package/src/{useCachedPrecommitValue.ts → useCachedResponsivePrecommitValue.ts} +18 -17
- package/src/useDisposableState.ts +12 -7
- package/src/useLazyDisposableState.test.tsx +70 -0
- package/src/useLazyDisposableState.ts +27 -15
- package/src/useUpdatableDisposableState.ts +1 -1
- package/tsconfig.json +6 -0
- package/src/useCachedPrecommitValue.test.tsx +0 -577
@@ -0,0 +1,571 @@
|
|
1
|
+
import { describe, test, vi, expect, assert } from 'vitest';
|
2
|
+
import { ParentCache } from './ParentCache';
|
3
|
+
import { ItemCleanupPair } from '@isograph/disposable-types';
|
4
|
+
import { useCachedResponsivePrecommitValue } from './useCachedResponsivePrecommitValue';
|
5
|
+
import React from 'react';
|
6
|
+
import { create } from 'react-test-renderer';
|
7
|
+
import { CacheItem, CacheItemState } from './CacheItem';
|
8
|
+
|
9
|
+
function getItem<T>(cache: ParentCache<T>): CacheItem<T> | null {
|
10
|
+
return (cache as any).__cacheItem;
|
11
|
+
}
|
12
|
+
|
13
|
+
function getState<T>(cacheItem: CacheItem<T>): CacheItemState<T> {
|
14
|
+
return (cacheItem as any).__state;
|
15
|
+
}
|
16
|
+
|
17
|
+
function Suspender({ promise, isResolvedRef }) {
|
18
|
+
if (!isResolvedRef.current) {
|
19
|
+
throw promise;
|
20
|
+
}
|
21
|
+
return null;
|
22
|
+
}
|
23
|
+
|
24
|
+
function shortPromise() {
|
25
|
+
let resolve;
|
26
|
+
const promise = new Promise((_resolve) => {
|
27
|
+
resolve = _resolve;
|
28
|
+
});
|
29
|
+
|
30
|
+
setTimeout(resolve, 1);
|
31
|
+
return promise;
|
32
|
+
}
|
33
|
+
|
34
|
+
function promiseAndResolver() {
|
35
|
+
let resolve;
|
36
|
+
const isResolvedRef = {
|
37
|
+
current: false,
|
38
|
+
};
|
39
|
+
const promise = new Promise((r) => {
|
40
|
+
resolve = r;
|
41
|
+
});
|
42
|
+
return {
|
43
|
+
promise,
|
44
|
+
resolve: () => {
|
45
|
+
isResolvedRef.current = true;
|
46
|
+
resolve();
|
47
|
+
},
|
48
|
+
isResolvedRef,
|
49
|
+
};
|
50
|
+
}
|
51
|
+
|
52
|
+
// The fact that sometimes we need to render in concurrent mode and sometimes
|
53
|
+
// not is a bit worrisome.
|
54
|
+
async function awaitableCreate(Component, isConcurrent) {
|
55
|
+
const element = create(
|
56
|
+
Component,
|
57
|
+
isConcurrent ? { unstable_isConcurrent: true } : undefined,
|
58
|
+
);
|
59
|
+
await shortPromise();
|
60
|
+
return element;
|
61
|
+
}
|
62
|
+
|
63
|
+
describe('useCachedResponsivePrecommitValue', () => {
|
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 = useCachedResponsivePrecommitValue(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 = useCachedResponsivePrecommitValue(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 = useCachedResponsivePrecommitValue(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
|
+
}
|
166
|
+
|
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 () => {
|
192
|
+
const disposeItem = vi.fn();
|
193
|
+
const factory = vi.fn(() => {
|
194
|
+
const pair: ItemCleanupPair<number> = [1, disposeItem];
|
195
|
+
return pair;
|
196
|
+
});
|
197
|
+
const cache = new ParentCache(factory);
|
198
|
+
const getOrPopulateAndTemporaryRetain = vi.spyOn(
|
199
|
+
cache,
|
200
|
+
'getOrPopulateAndTemporaryRetain',
|
201
|
+
);
|
202
|
+
|
203
|
+
const componentCommits = vi.fn();
|
204
|
+
const hookOnCommit = vi.fn();
|
205
|
+
const render = vi.fn();
|
206
|
+
let renderCount = 0;
|
207
|
+
function TestComponent() {
|
208
|
+
render();
|
209
|
+
const value = useCachedResponsivePrecommitValue(cache, hookOnCommit);
|
210
|
+
|
211
|
+
expect(value).toEqual({ state: 1 });
|
212
|
+
expect(factory).toHaveBeenCalledTimes(1);
|
213
|
+
|
214
|
+
renderCount++;
|
215
|
+
expect(getOrPopulateAndTemporaryRetain).toHaveBeenCalledTimes(
|
216
|
+
renderCount,
|
217
|
+
);
|
218
|
+
|
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
|
+
}, []);
|
226
|
+
|
227
|
+
return <div />;
|
228
|
+
}
|
229
|
+
|
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
|
+
);
|
238
|
+
|
239
|
+
expect(componentCommits).toHaveBeenCalledTimes(0);
|
240
|
+
expect(render).toHaveBeenCalledTimes(1);
|
241
|
+
|
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 () => {
|
254
|
+
const disposeItem = vi.fn();
|
255
|
+
let factoryValue = 0;
|
256
|
+
const factory = vi.fn(() => {
|
257
|
+
factoryValue++;
|
258
|
+
const pair: ItemCleanupPair<number> = [factoryValue, disposeItem];
|
259
|
+
return pair;
|
260
|
+
});
|
261
|
+
const cache = new ParentCache(factory);
|
262
|
+
|
263
|
+
const getOrPopulateAndTemporaryRetain = vi.spyOn(
|
264
|
+
cache,
|
265
|
+
'getOrPopulateAndTemporaryRetain',
|
266
|
+
);
|
267
|
+
|
268
|
+
const componentCommits = vi.fn();
|
269
|
+
const hookOnCommit = vi.fn();
|
270
|
+
const render = vi.fn();
|
271
|
+
function TestComponent() {
|
272
|
+
render();
|
273
|
+
const value = useCachedResponsivePrecommitValue(cache, hookOnCommit);
|
274
|
+
|
275
|
+
expect(value).toEqual({ state: factoryValue });
|
276
|
+
|
277
|
+
React.useEffect(() => {
|
278
|
+
componentCommits();
|
279
|
+
expect(cache.isEmpty()).toBe(true);
|
280
|
+
expect(componentCommits).toHaveBeenCalledTimes(1);
|
281
|
+
expect(hookOnCommit).toBeCalledTimes(1);
|
282
|
+
expect(hookOnCommit.mock.calls[0][0][0]).toBe(2);
|
283
|
+
expect(factory).toBeCalledTimes(2);
|
284
|
+
expect(disposeItem).toBeCalledTimes(1);
|
285
|
+
}, []);
|
286
|
+
|
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
|
+
|
299
|
+
return <div />;
|
300
|
+
}
|
301
|
+
|
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);
|
312
|
+
expect(render).toHaveBeenCalledTimes(1);
|
313
|
+
|
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 () => {
|
327
|
+
const disposeItem = vi.fn();
|
328
|
+
let factoryCount = 0;
|
329
|
+
const factory = vi.fn(() => {
|
330
|
+
factoryCount++;
|
331
|
+
const pair: ItemCleanupPair<number> = [factoryCount, disposeItem];
|
332
|
+
return pair;
|
333
|
+
});
|
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
|
+
);
|
343
|
+
|
344
|
+
const componentCommits = vi.fn();
|
345
|
+
const hookOnCommit = vi.fn();
|
346
|
+
const render = vi.fn();
|
347
|
+
function TestComponent() {
|
348
|
+
render();
|
349
|
+
|
350
|
+
useCachedResponsivePrecommitValue(cache, hookOnCommit);
|
351
|
+
|
352
|
+
React.useEffect(() => {
|
353
|
+
componentCommits();
|
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);
|
363
|
+
}, []);
|
364
|
+
|
365
|
+
return <div />;
|
366
|
+
}
|
367
|
+
|
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);
|
388
|
+
|
389
|
+
expect(render).toHaveBeenCalledTimes(1);
|
390
|
+
expect(hookOnCommit).toBeCalledTimes(0);
|
391
|
+
expect(componentCommits).toBeCalledTimes(0);
|
392
|
+
expect(factory).toHaveBeenCalledTimes(1);
|
393
|
+
|
394
|
+
initialRender = false;
|
395
|
+
}
|
396
|
+
|
397
|
+
return null;
|
398
|
+
}
|
399
|
+
|
400
|
+
const element = await awaitableCreate(
|
401
|
+
<>
|
402
|
+
<TestComponent />
|
403
|
+
<CodeExecutor />
|
404
|
+
</>,
|
405
|
+
false,
|
406
|
+
);
|
407
|
+
|
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
|
+
);
|
414
|
+
|
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
|
+
);
|
432
|
+
|
433
|
+
const componentCommits = vi.fn();
|
434
|
+
const hookOnCommit = vi.fn();
|
435
|
+
const render = vi.fn();
|
436
|
+
function TestComponent() {
|
437
|
+
render();
|
438
|
+
useCachedResponsivePrecommitValue(cache, hookOnCommit);
|
439
|
+
|
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
|
+
}, []);
|
452
|
+
|
453
|
+
return <div />;
|
454
|
+
}
|
455
|
+
|
456
|
+
const getOrPopulateAndTemporaryRetain = vi.spyOn(
|
457
|
+
cache,
|
458
|
+
'getOrPopulateAndTemporaryRetain',
|
459
|
+
);
|
460
|
+
|
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);
|
481
|
+
|
482
|
+
cache.getOrPopulateAndTemporaryRetain();
|
483
|
+
expect(cache.isEmpty()).toBe(false);
|
484
|
+
// The factory function was called when we called getOrPopulateAndTemporaryRetain
|
485
|
+
expect(factory).toHaveBeenCalledTimes(2);
|
486
|
+
|
487
|
+
expect(render).toHaveBeenCalledTimes(1);
|
488
|
+
expect(hookOnCommit).toBeCalledTimes(0);
|
489
|
+
expect(componentCommits).toBeCalledTimes(0);
|
490
|
+
|
491
|
+
initialRender = false;
|
492
|
+
}
|
493
|
+
|
494
|
+
return null;
|
495
|
+
}
|
496
|
+
|
497
|
+
const element = await awaitableCreate(
|
498
|
+
<React.Suspense fallback="fallback">
|
499
|
+
<TestComponent />
|
500
|
+
<CodeExecutor />
|
501
|
+
</React.Suspense>,
|
502
|
+
false,
|
503
|
+
);
|
504
|
+
|
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
|
+
);
|
513
|
+
|
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);
|
524
|
+
|
525
|
+
const componentCommits = vi.fn();
|
526
|
+
const hookOnCommit = vi.fn();
|
527
|
+
const render = vi.fn();
|
528
|
+
function TestComponent() {
|
529
|
+
render();
|
530
|
+
|
531
|
+
useCachedResponsivePrecommitValue(cache, hookOnCommit);
|
532
|
+
|
533
|
+
React.useEffect(() => {
|
534
|
+
componentCommits();
|
535
|
+
}, []);
|
536
|
+
|
537
|
+
return <div />;
|
538
|
+
}
|
539
|
+
|
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
|
+
);
|
559
|
+
|
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
|
+
});
|
@@ -1,20 +1,19 @@
|
|
1
1
|
'use strict';
|
2
2
|
|
3
|
-
import {
|
3
|
+
import { ItemCleanupPair } from '@isograph/disposable-types';
|
4
|
+
import { useEffect, useRef, useState } from 'react';
|
4
5
|
import { ParentCache } from './ParentCache';
|
5
|
-
import { useHasCommittedRef } from './useHasCommittedRef';
|
6
|
-
import { ItemCleanupPair } from '@isograph/isograph-disposable-types/dist';
|
7
6
|
|
8
7
|
/**
|
9
|
-
*
|
8
|
+
* useCachedResponsivePrecommitValue<T>
|
10
9
|
* - Takes a mutable parent cache, a factory function, and an onCommit callback.
|
11
|
-
* - Returns T before
|
12
|
-
* - Calls onCommit with the ItemCleanupPair during
|
10
|
+
* - Returns T before commit after every parent cache change, and null afterward.
|
11
|
+
* - Calls onCommit with the ItemCleanupPair during commit after every parent cache change.
|
13
12
|
* - The T from the render phase is only temporarily retained. It may have been
|
14
13
|
* disposed by the time of the commit. If so, this hook checks the parent cache
|
15
14
|
* for another T or creates one, and passes this T to onCommit.
|
16
15
|
* - 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.
|
16
|
+
* is passed to onCommit, during the commit phase, it will schedule another render.
|
18
17
|
*
|
19
18
|
* Invariant: the returned T has not been disposed during the tick of the render.
|
20
19
|
* The T passed to the onCommit callback has not been disposed when the onCommit
|
@@ -26,17 +25,19 @@ import { ItemCleanupPair } from '@isograph/isograph-disposable-types/dist';
|
|
26
25
|
* with a set of variables (e.g. {name: "Matthew"}), and pass in another cache
|
27
26
|
* (e.g. associated with {name: "James"}), which is empty, the hook will fill that
|
28
27
|
* new cache with the factory function.
|
28
|
+
* - Post-commit, passing a different parentCache will reset hook to the pre-commit
|
29
|
+
* state. The cache will return T before commit, then fill the new cache with the
|
30
|
+
* factory function and return null afterwards.
|
29
31
|
*
|
30
32
|
* Passing a different factory:
|
31
33
|
* - Passing a different factory has no effect, except when factory is called,
|
32
|
-
* which is when the parent cache is being filled, or during
|
34
|
+
* which is when the parent cache is being filled, or during commit.
|
33
35
|
*
|
34
36
|
* Passing a different onCommit:
|
35
|
-
* - Passing a different onCommit has no effect, except for during
|
37
|
+
* - Passing a different onCommit has no effect, except for during commit.
|
36
38
|
*
|
37
|
-
* Post-commit, all parameters are ignored and the hook returns null.
|
38
39
|
*/
|
39
|
-
export function
|
40
|
+
export function useCachedResponsivePrecommitValue<T>(
|
40
41
|
parentCache: ParentCache<T>,
|
41
42
|
onCommit: (pair: ItemCleanupPair<T>) => void,
|
42
43
|
): { state: T } | null {
|
@@ -47,9 +48,11 @@ export function useCachedPrecommitValue<T>(
|
|
47
48
|
//
|
48
49
|
// This hook is the former, i.e. re-renders if the committed item has changed.
|
49
50
|
const [, rerender] = useState<{} | null>(null);
|
51
|
+
const lastCommittedParentCache = useRef<ParentCache<T> | null>(null);
|
50
52
|
|
51
53
|
useEffect(() => {
|
52
|
-
|
54
|
+
lastCommittedParentCache.current = parentCache;
|
55
|
+
// On commit, cacheItem may be disposed, because during the render phase,
|
53
56
|
// we only temporarily retained the item, and the temporary retain could have
|
54
57
|
// expired by the time of the commit.
|
55
58
|
//
|
@@ -81,21 +84,19 @@ export function useCachedPrecommitValue<T>(
|
|
81
84
|
// We did not find an item in the parent cache, create a new one.
|
82
85
|
onCommit(parentCache.factory());
|
83
86
|
}
|
84
|
-
|
85
87
|
// TODO: Consider whether we always want to rerender if the committed item
|
86
88
|
// was not returned during the last render, or whether some callers will
|
87
89
|
// prefer opting out of this behavior (e.g. if every disposable item behaves
|
88
90
|
// identically, but must be loaded.)
|
89
91
|
rerender({});
|
90
92
|
}
|
91
|
-
}, []);
|
93
|
+
}, [parentCache]);
|
92
94
|
|
93
|
-
|
94
|
-
if (hasCommittedRef.current) {
|
95
|
+
if (lastCommittedParentCache.current === parentCache) {
|
95
96
|
return null;
|
96
97
|
}
|
97
98
|
|
98
|
-
// Safety: item is only safe to use (i.e. guaranteed not to have disposed)
|
99
|
+
// Safety: item is only safe to use (i.e. guaranteed not to have been disposed)
|
99
100
|
// during this tick.
|
100
101
|
const [cacheItem, item, disposeOfTemporaryRetain] =
|
101
102
|
parentCache.getOrPopulateAndTemporaryRetain();
|
@@ -1,7 +1,8 @@
|
|
1
|
+
import { ItemCleanupPair } from '@isograph/disposable-types';
|
1
2
|
import { useEffect, useRef } from 'react';
|
2
3
|
import { ParentCache } from './ParentCache';
|
3
|
-
|
4
|
-
import {
|
4
|
+
|
5
|
+
import { useCachedResponsivePrecommitValue } from './useCachedResponsivePrecommitValue';
|
5
6
|
import {
|
6
7
|
UNASSIGNED_STATE,
|
7
8
|
UnassignedState,
|
@@ -18,9 +19,13 @@ export function useDisposableState<T = never>(
|
|
18
19
|
): UseUpdatableDisposableStateReturnValue<T> {
|
19
20
|
const itemCleanupPairRef = useRef<ItemCleanupPair<T> | null>(null);
|
20
21
|
|
21
|
-
const preCommitItem =
|
22
|
-
|
23
|
-
|
22
|
+
const preCommitItem = useCachedResponsivePrecommitValue(
|
23
|
+
parentCache,
|
24
|
+
(pair) => {
|
25
|
+
itemCleanupPairRef.current?.[1]();
|
26
|
+
itemCleanupPairRef.current = pair;
|
27
|
+
},
|
28
|
+
);
|
24
29
|
|
25
30
|
const { state: stateFromDisposableStateHook, setState } =
|
26
31
|
useUpdatableDisposableState<T>();
|
@@ -68,8 +73,8 @@ export function useDisposableState<T = never>(
|
|
68
73
|
(stateFromDisposableStateHook != UNASSIGNED_STATE
|
69
74
|
? stateFromDisposableStateHook
|
70
75
|
: null) ??
|
71
|
-
|
72
|
-
|
76
|
+
preCommitItem?.state ??
|
77
|
+
itemCleanupPairRef.current?.[0];
|
73
78
|
|
74
79
|
return {
|
75
80
|
state: state!,
|