1
let React;
2
let ReactNoop;
3
let Cache;
4
+let getCacheSignal;
5
let getCacheForType;
6
let Scheduler;
7
let act;
23
Scheduler = require('scheduler');
24
act = require('jest-react').act;
25
Suspense = React.Suspense;
26
+ getCacheSignal = React.unstable_getCacheSignal;
27
getCacheForType = React.unstable_getCacheForType;
28
useCacheRefresh = React.unstable_useCacheRefresh;
29
startTransition = React.startTransition;
54
const newRecord = {
55
status: 'resolved',
56
value: text,
57
+ cleanupScheduled: false,
58
};
59
data.set(text, newRecord);
60
} else if (record.status === 'pending') {
67
const newRecord = {
68
status: 'rejected',
69
value: error,
70
+ cleanupScheduled: false,
71
};
72
data.set(text, newRecord);
73
} else if (record.status === 'pending') {
80
}
81
82
function readText(text) {
83
+ const signal = getCacheSignal();
84
const textCache = getCacheForType(createTextCache);
85
const record = textCache.data.get(text);
86
if (record !== undefined) {
87
+ if (!record.cleanupScheduled) {
88
+ // This record was seeded prior to the abort signal being available:
89
+ // schedule a cleanup function for it.
90
+ // TODO: Add ability to cleanup entries seeded w useCacheRefresh()
91
+ record.cleanupScheduled = true;
92
+ signal.addEventListener('abort', () => {
93
+ Scheduler.unstable_yieldValue(
94
+ `Cache cleanup: ${text} [v${textCache.version}]`,
95
+ );
96
+ });
97
+ }
98
switch (record.status) {
99
case 'pending':
100
throw record.value;
131
const newRecord = {
132
status: 'pending',
133
value: thenable,
134
+ cleanupScheduled: true,
135
};
136
textCache.data.set(text, newRecord);
137
138
+ signal.addEventListener('abort', () => {
139
+ Scheduler.unstable_yieldValue(
140
+ `Cache cleanup: ${text} [v${textCache.version}]`,
141
+ );
142
+ });
143
throw thenable;
144
}
145
}
202
});
203
expect(Scheduler).toHaveYielded(['A']);
204
expect(root).toMatchRenderedOutput('A');
205
+
206
+ await act(async () => {
207
+ root.render('Bye');
208
+ });
209
+ // no cleanup: cache is still retained at the root
210
+ expect(Scheduler).toHaveYielded([]);
211
+ expect(root).toMatchRenderedOutput('Bye');
212
});
213
214
// @gate experimental || www
229
});
230
expect(Scheduler).toHaveYielded(['A']);
231
expect(root).toMatchRenderedOutput('A');
232
+
233
+ await act(async () => {
234
+ root.render('Bye');
235
+ });
236
+ // no cleanup: cache is still retained at the root
237
+ expect(Scheduler).toHaveYielded([]);
238
+ expect(root).toMatchRenderedOutput('Bye');
239
});
240
241
// @gate experimental || www
242
test('multiple new Cache boundaries in the same update share the same, fresh cache', async () => {
207
- function App({text}) {
208
- return (
243
+ function App({showMore}) {
244
+ return showMore ? (
245
<>
246
<Cache>
247
<Suspense fallback={<Text text="Loading..." />}>
254
</Suspense>
255
</Cache>
256
</>
257
+ ) : (
258
+ '(empty)'
259
);
260
}
261
263
await act(async () => {
264
root.render(<App showMore={false} />);
265
});
266
+ expect(Scheduler).toHaveYielded([]);
267
+ expect(root).toMatchRenderedOutput('(empty)');
268
+
269
+ await act(async () => {
270
+ root.render(<App showMore={true} />);
271
+ });
272
// Even though there are two new <Cache /> trees, they should share the same
273
// data cache. So there should be only a single cache miss for A.
274
expect(Scheduler).toHaveYielded([
283
});
284
expect(Scheduler).toHaveYielded(['A', 'A']);
285
expect(root).toMatchRenderedOutput('AA');
286
+
287
+ await act(async () => {
288
+ root.render('Bye');
289
+ });
290
+ // cleanup occurs for the cache shared by the inner cache boundaries (which
291
+ // are not shared w the root because they were added in an update)
292
+ // note that no cache is created for the root since the cache is never accessed
293
+ expect(Scheduler).toHaveYielded(['Cache cleanup: A [v1]']);
294
+ expect(root).toMatchRenderedOutput('Bye');
295
});
296
297
// @gate experimental || www
314
await act(async () => {
315
root.render(<App />);
316
});
264
- // Even though there are two new <Cache /> trees, they should share the same
265
- // data cache. So there should be only a single cache miss for A.
317
+ // Even though there is a nested <Cache /> boundary, it should share the same
318
+ // data cache as the root. So there should be only a single cache miss for A.
319
expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
320
expect(root).toMatchRenderedOutput('Loading...');
321
324
});
325
expect(Scheduler).toHaveYielded(['A', 'A']);
326
expect(root).toMatchRenderedOutput('AA');
327
+
328
+ await act(async () => {
329
+ root.render('Bye');
330
+ });
331
+ // no cleanup: cache is still retained at the root
332
+ expect(Scheduler).toHaveYielded([]);
333
+ expect(root).toMatchRenderedOutput('Bye');
334
},
335
);
336
369
'A [v1]',
370
]);
371
expect(root).toMatchRenderedOutput('A [v1]A [v1]');
372
+
373
+ await act(async () => {
374
+ root.render('Bye');
375
+ });
376
+ // no cleanup: cache is still retained at the root
377
+ expect(Scheduler).toHaveYielded([]);
378
+ expect(root).toMatchRenderedOutput('Bye');
379
});
380
381
// @gate experimental || www
423
});
424
expect(Scheduler).toHaveYielded(['A [v2]']);
425
expect(root).toMatchRenderedOutput('A [v1]A [v2]');
426
+
427
+ // Replace all the children: this should retain the root Cache instance,
428
+ // but cleanup the separate cache instance created for the fresh cache
429
+ // boundary
430
+ await act(async () => {
431
+ root.render('Bye!');
432
+ });
433
+ // Cleanup occurs for the *second* cache instance: the first is still
434
+ // referenced by the root
435
+ expect(Scheduler).toHaveYielded(['Cache cleanup: A [v2]']);
436
+ expect(root).toMatchRenderedOutput('Bye!');
437
});
438
439
// @gate experimental || www
362
- test('inner content uses same cache as shell if spawned by the same transition', async () => {
440
+ test('inner/outer cache boundaries uses the same cache instance on initial render', async () => {
441
const root = ReactNoop.createRoot();
442
443
function App() {
509
<div>Content</div>
510
</>,
511
);
512
+
513
+ await act(async () => {
514
+ root.render('Bye');
515
+ });
516
+ // no cleanup: cache is still retained at the root
517
+ expect(Scheduler).toHaveYielded([]);
518
+ expect(root).toMatchRenderedOutput('Bye');
519
+ });
520
+
521
+ // @gate experimental || www
522
+ test('inner/ outer cache boundaries added in the same update use the same cache instance', async () => {
523
+ const root = ReactNoop.createRoot();
524
+
525
+ function App({showMore}) {
526
+ return showMore ? (
527
+ <Cache>
528
+ <Suspense fallback={<Text text="Loading shell..." />}>
529
+ {/* The shell reads A */}
530
+ <Shell>
531
+ {/* The inner content reads both A and B */}
532
+ <Suspense fallback={<Text text="Loading content..." />}>
533
+ <Cache>
534
+ <Content />
535
+ </Cache>
536
+ </Suspense>
537
+ </Shell>
538
+ </Suspense>
539
+ </Cache>
540
+ ) : (
541
+ '(empty)'
542
+ );
543
+ }
544
+
545
+ function Shell({children}) {
546
+ readText('A');
547
+ return (
548
+ <>
549
+ <div>
550
+ <Text text="Shell" />
551
+ </div>
552
+ <div>{children}</div>
553
+ </>
554
+ );
555
+ }
556
+
557
+ function Content() {
558
+ readText('A');
559
+ readText('B');
560
+ return <Text text="Content" />;
561
+ }
562
+
563
+ await act(async () => {
564
+ root.render(<App showMore={false} />);
565
+ });
566
+ expect(Scheduler).toHaveYielded([]);
567
+ expect(root).toMatchRenderedOutput('(empty)');
568
+
569
+ await act(async () => {
570
+ root.render(<App showMore={true} />);
571
+ });
572
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading shell...']);
573
+ expect(root).toMatchRenderedOutput('Loading shell...');
574
+
575
+ await act(async () => {
576
+ resolveMostRecentTextCache('A');
577
+ });
578
+ expect(Scheduler).toHaveYielded([
579
+ 'Shell',
580
+ // There's a cache miss for B, because it hasn't been read yet. But not
581
+ // A, because it was cached when we rendered the shell.
582
+ 'Cache miss! [B]',
583
+ 'Loading content...',
584
+ ]);
585
+ expect(root).toMatchRenderedOutput(
586
+ <>
587
+ <div>Shell</div>
588
+ <div>Loading content...</div>
589
+ </>,
590
+ );
591
+
592
+ await act(async () => {
593
+ resolveMostRecentTextCache('B');
594
+ });
595
+ expect(Scheduler).toHaveYielded(['Content']);
596
+ expect(root).toMatchRenderedOutput(
597
+ <>
598
+ <div>Shell</div>
599
+ <div>Content</div>
600
+ </>,
601
+ );
602
+
603
+ await act(async () => {
604
+ root.render('Bye');
605
+ });
606
+ expect(Scheduler).toHaveYielded([
607
+ 'Cache cleanup: A [v1]',
608
+ 'Cache cleanup: B [v1]',
609
+ ]);
610
+ expect(root).toMatchRenderedOutput('Bye');
611
});
612
613
// @gate experimental || www
437
- test('refresh a cache', async () => {
614
+ test('refresh a cache boundary', async () => {
615
let refresh;
616
function App() {
617
refresh = useCacheRefresh();
651
// Note that the version has updated
652
expect(Scheduler).toHaveYielded(['A [v2]']);
653
expect(root).toMatchRenderedOutput('A [v2]');
654
+
655
+ await act(async () => {
656
+ root.render('Bye');
657
+ });
658
+ // the original cache instance does not cleanup since it is still referenced
659
+ // by the root, but the refreshed inner cache does cleanup
660
+ expect(Scheduler).toHaveYielded(['Cache cleanup: A [v2]']);
661
+ expect(root).toMatchRenderedOutput('Bye');
662
});
663
664
// @gate experimental || www
697
await act(async () => {
698
resolveMostRecentTextCache('A');
699
});
515
- // Note that the version has updated
516
- expect(Scheduler).toHaveYielded(['A [v2]']);
700
+ // Note that the version has updated, and the previous cache is cleared
701
+ expect(Scheduler).toHaveYielded(['A [v2]', 'Cache cleanup: A [v1]']);
702
expect(root).toMatchRenderedOutput('A [v2]');
703
+
704
+ await act(async () => {
705
+ root.render('Bye');
706
+ });
707
+ // the original root cache already cleaned up when the refresh completed
708
+ expect(Scheduler).toHaveYielded([]);
709
+ expect(root).toMatchRenderedOutput('Bye');
710
+ });
711
+
712
+ // @gate experimental || www
713
+ test('refresh the root cache without a transition', async () => {
714
+ let refresh;
715
+ function App() {
716
+ refresh = useCacheRefresh();
717
+ return <AsyncText showVersion={true} text="A" />;
718
+ }
719
+
720
+ // Mount initial data
721
+ const root = ReactNoop.createRoot();
722
+ await act(async () => {
723
+ root.render(
724
+ <Suspense fallback={<Text text="Loading..." />}>
725
+ <App />
726
+ </Suspense>,
727
+ );
728
+ });
729
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
730
+ expect(root).toMatchRenderedOutput('Loading...');
731
+
732
+ await act(async () => {
733
+ resolveMostRecentTextCache('A');
734
+ });
735
+ expect(Scheduler).toHaveYielded(['A [v1]']);
736
+ expect(root).toMatchRenderedOutput('A [v1]');
737
+
738
+ // Refresh for new data.
739
+ await act(async () => {
740
+ refresh();
741
+ });
742
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]', 'Loading...']);
743
+ expect(root).toMatchRenderedOutput('Loading...');
744
+
745
+ await act(async () => {
746
+ resolveMostRecentTextCache('A');
747
+ });
748
+ // Note that the version has updated, and the previous cache is cleared
749
+ expect(Scheduler).toHaveYielded(['A [v2]', 'Cache cleanup: A [v1]']);
750
+ expect(root).toMatchRenderedOutput('A [v2]');
751
+
752
+ await act(async () => {
753
+ root.render('Bye');
754
+ });
755
+ // the original root cache already cleaned up when the refresh completed
756
+ expect(Scheduler).toHaveYielded([]);
757
+ expect(root).toMatchRenderedOutput('Bye');
758
});
759
760
// @gate experimental || www
796
startTransition(() => refresh(createTextCache, cache));
797
});
798
// The root should re-render without a cache miss.
799
+ // The cache is not cleared up yet, since it's still reference by the root
800
expect(Scheduler).toHaveYielded(['A [v2]']);
801
expect(root).toMatchRenderedOutput('A [v2]');
802
+
803
+ await act(async () => {
804
+ root.render('Bye');
805
+ });
806
+ // the refreshed cache boundary is unmounted and cleans up
807
+ expect(Scheduler).toHaveYielded(['Cache cleanup: A [v2]']);
808
+ expect(root).toMatchRenderedOutput('Bye');
809
});
810
811
// @gate experimental || www
869
await act(async () => {
870
resolveMostRecentTextCache('A');
871
});
624
- expect(Scheduler).toHaveYielded(['A [v3]', 'A [v3]']);
872
+ expect(Scheduler).toHaveYielded([
873
+ 'A [v3]',
874
+ 'A [v3]',
875
+ // once the refresh completes the inner showMore boundary frees its previous
876
+ // cache instance, since it is now using the refreshed parent instance.
877
+ 'Cache cleanup: A [v2]',
878
+ ]);
879
expect(root).toMatchRenderedOutput('A [v3]A [v3]');
880
+
881
+ await act(async () => {
882
+ root.render('Bye!');
883
+ });
884
+ // Unmounting children releases the refreshed cache instance only; the root
885
+ // still retains the original cache instance used for the first render
886
+ expect(Scheduler).toHaveYielded(['Cache cleanup: A [v3]']);
887
+ expect(root).toMatchRenderedOutput('Bye!');
888
});
889
890
// @gate experimental || www
957
});
958
expect(Scheduler).toHaveYielded(['A [v2]']);
959
expect(root).toMatchRenderedOutput('A [v2]A [v1]');
960
+
961
+ // Unmount children: this should clear *both* cache instances:
962
+ // the root doesn't have a cache instance (since it wasn't accessed
963
+ // during the initial render, and all subsequent cache accesses were within
964
+ // a fresh boundary). Therefore this causes cleanup for both the fresh cache
965
+ // instance in the refreshed first boundary and cleanup for the non-refreshed
966
+ // sibling boundary.
967
+ await act(async () => {
968
+ root.render('Bye!');
969
+ });
970
+ expect(Scheduler).toHaveYielded([
971
+ 'Cache cleanup: A [v2]',
972
+ 'Cache cleanup: A [v1]',
973
+ ]);
974
+ expect(root).toMatchRenderedOutput('Bye!');
975
},
976
);
977
1010
'Cache miss! [B]',
1011
'Loading...',
1012
]);
1013
+ expect(root).toMatchRenderedOutput('Loading...');
1014
1015
await act(async () => {
1016
// This will resolve the content in the first cache
1028
'A [v1]',
1029
'B [v1]',
1030
]);
1031
+ expect(root).toMatchRenderedOutput('Loading... A [v1] B [v1]');
1032
1033
// Now resolve the second tree
1034
await act(async () => {
1036
});
1037
expect(Scheduler).toHaveYielded(['A [v2]']);
1038
expect(root).toMatchRenderedOutput('A [v2] A [v1] B [v1]');
1039
+
1040
+ await act(async () => {
1041
+ root.render('Bye!');
1042
+ });
1043
+ // Unmounting children releases both cache boundaries, but the original
1044
+ // cache instance (used by second boundary) is still referenced by the root.
1045
+ // only the second cache instance is freed.
1046
+ expect(Scheduler).toHaveYielded(['Cache cleanup: A [v2]']);
1047
+ expect(root).toMatchRenderedOutput('Bye!');
1048
},
1049
);
1050
1129
});
1130
expect(Scheduler).toHaveYielded(['A [v1]', 'A [v1]', 'A [v2]']);
1131
expect(root).toMatchRenderedOutput('A [v1]A [v1]A [v2]');
1132
+
1133
+ // Unmount children: the first text cache instance is created only after the root
1134
+ // commits, so both fresh cache instances are released by their cache boundaries,
1135
+ // cleaning up v1 (used for the first two children which render togeether) and
1136
+ // v2 (used for the third boundary added later).
1137
+ await act(async () => {
1138
+ root.render('Bye!');
1139
+ });
1140
+ expect(Scheduler).toHaveYielded([
1141
+ 'Cache cleanup: A [v1]',
1142
+ 'Cache cleanup: A [v2]',
1143
+ ]);
1144
+ expect(root).toMatchRenderedOutput('Bye!');
1145
});
1146
1147
// @gate experimental || www
1164
<Suspense fallback={<Text text="Loading..." />}>
1165
{shouldShow ? (
1166
<Cache>
866
- <AsyncText text="A" />
1167
+ <AsyncText showVersion={true} text="A" />
1168
</Cache>
1169
) : null}
1170
</Suspense>
1181
1182
const root = ReactNoop.createRoot();
1183
await act(async () => {
883
- root.render(<App showMore={false} />);
1184
+ root.render(<App />);
1185
});
1186
expect(Scheduler).toHaveYielded(['0']);
1187
expect(root).toMatchRenderedOutput('0');
1209
await act(async () => {
1210
resolveMostRecentTextCache('A');
1211
});
911
- expect(Scheduler).toHaveYielded(['A']);
912
- expect(root).toMatchRenderedOutput('A1');
1212
+ expect(Scheduler).toHaveYielded(['A [v1]']);
1213
+ expect(root).toMatchRenderedOutput('A [v1]1');
1214
+
1215
+ // Unmount children: the first text cache instance is created only after initial
1216
+ // render after calling showMore(). This instance is cleaned up when that boundary
1217
+ // is unmounted. Bc root cache instance is never accessed, the inner cache
1218
+ // boundary ends up at v1.
1219
+ await act(async () => {
1220
+ root.render('Bye!');
1221
+ });
1222
+ expect(Scheduler).toHaveYielded(['Cache cleanup: A [v1]']);
1223
+ expect(root).toMatchRenderedOutput('Bye!');
1224
+ });
1225
+
1226
+ // @gate experimental || www
1227
+ test('cache boundary uses a fresh cache when its key changes', async () => {
1228
+ const root = ReactNoop.createRoot();
1229
+ seedNextTextCache('A');
1230
+ await act(async () => {
1231
+ root.render(
1232
+ <Suspense fallback="Loading...">
1233
+ <Cache key="A">
1234
+ <AsyncText showVersion={true} text="A" />
1235
+ </Cache>
1236
+ </Suspense>,
1237
+ );
1238
+ });
1239
+ expect(Scheduler).toHaveYielded(['A [v1]']);
1240
+ expect(root).toMatchRenderedOutput('A [v1]');
1241
+
1242
+ seedNextTextCache('B');
1243
+ await act(async () => {
1244
+ root.render(
1245
+ <Suspense fallback="Loading...">
1246
+ <Cache key="B">
1247
+ <AsyncText showVersion={true} text="B" />
1248
+ </Cache>
1249
+ </Suspense>,
1250
+ );
1251
+ });
1252
+ expect(Scheduler).toHaveYielded(['B [v2]']);
1253
+ expect(root).toMatchRenderedOutput('B [v2]');
1254
+
1255
+ // Unmount children: the fresh cache instance for B cleans up since the cache boundary
1256
+ // is the only owner, while the original cache instance (for A) is still retained by
1257
+ // the root.
1258
+ await act(async () => {
1259
+ root.render('Bye!');
1260
+ });
1261
+ expect(Scheduler).toHaveYielded(['Cache cleanup: B [v2]']);
1262
+ expect(root).toMatchRenderedOutput('Bye!');
1263
+ });
1264
+
1265
+ // @gate experimental || www
1266
+ test('overlapping transitions after an initial mount use the same fresh cache', async () => {
1267
+ const root = ReactNoop.createRoot();
1268
+ await act(async () => {
1269
+ root.render(
1270
+ <Suspense fallback="Loading...">
1271
+ <Cache key="A">
1272
+ <AsyncText showVersion={true} text="A" />
1273
+ </Cache>
1274
+ </Suspense>,
1275
+ );
1276
+ });
1277
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]']);
1278
+ expect(root).toMatchRenderedOutput('Loading...');
1279
+
1280
+ await act(async () => {
1281
+ resolveMostRecentTextCache('A');
1282
+ });
1283
+ expect(Scheduler).toHaveYielded(['A [v1]']);
1284
+ expect(root).toMatchRenderedOutput('A [v1]');
1285
+
1286
+ // After a mount, subsequent transitions use a fresh cache
1287
+ await act(async () => {
1288
+ startTransition(() => {
1289
+ root.render(
1290
+ <Suspense fallback="Loading...">
1291
+ <Cache key="B">
1292
+ <AsyncText showVersion={true} text="B" />
1293
+ </Cache>
1294
+ </Suspense>,
1295
+ );
1296
+ });
1297
+ });
1298
+ expect(Scheduler).toHaveYielded(['Cache miss! [B]']);
1299
+ expect(root).toMatchRenderedOutput('A [v1]');
1300
+
1301
+ // Update to a different text and with a different key for the cache
1302
+ // boundary: this should still use the fresh cache instance created
1303
+ // for the earlier transition
1304
+ await act(async () => {
1305
+ startTransition(() => {
1306
+ root.render(
1307
+ <Suspense fallback="Loading...">
1308
+ <Cache key="C">
1309
+ <AsyncText showVersion={true} text="C" />
1310
+ </Cache>
1311
+ </Suspense>,
1312
+ );
1313
+ });
1314
+ });
1315
+ expect(Scheduler).toHaveYielded(['Cache miss! [C]']);
1316
+ expect(root).toMatchRenderedOutput('A [v1]');
1317
+
1318
+ await act(async () => {
1319
+ resolveMostRecentTextCache('C');
1320
+ });
1321
+ expect(Scheduler).toHaveYielded(['C [v2]']);
1322
+ expect(root).toMatchRenderedOutput('C [v2]');
1323
+
1324
+ // Unmount children: the fresh cache used for the updates is freed, while the
1325
+ // original cache (with A) is still retained at the root.
1326
+ await act(async () => {
1327
+ root.render('Bye!');
1328
+ });
1329
+ expect(Scheduler).toHaveYielded([
1330
+ 'Cache cleanup: B [v2]',
1331
+ 'Cache cleanup: C [v2]',
1332
+ ]);
1333
+ expect(root).toMatchRenderedOutput('Bye!');
1334
+ });
1335
+
1336
+ // @gate experimental || www
1337
+ test('overlapping updates after an initial mount use the same fresh cache', async () => {
1338
+ const root = ReactNoop.createRoot();
1339
+ await act(async () => {
1340
+ root.render(
1341
+ <Suspense fallback="Loading...">
1342
+ <Cache key="A">
1343
+ <AsyncText showVersion={true} text="A" />
1344
+ </Cache>
1345
+ </Suspense>,
1346
+ );
1347
+ });
1348
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]']);
1349
+ expect(root).toMatchRenderedOutput('Loading...');
1350
+
1351
+ await act(async () => {
1352
+ resolveMostRecentTextCache('A');
1353
+ });
1354
+ expect(Scheduler).toHaveYielded(['A [v1]']);
1355
+ expect(root).toMatchRenderedOutput('A [v1]');
1356
+
1357
+ // After a mount, subsequent updates use a fresh cache
1358
+ await act(async () => {
1359
+ root.render(
1360
+ <Suspense fallback="Loading...">
1361
+ <Cache key="B">
1362
+ <AsyncText showVersion={true} text="B" />
1363
+ </Cache>
1364
+ </Suspense>,
1365
+ );
1366
+ });
1367
+ expect(Scheduler).toHaveYielded(['Cache miss! [B]']);
1368
+ expect(root).toMatchRenderedOutput('Loading...');
1369
+
1370
+ // A second update uses the same fresh cache: even though this is a new
1371
+ // Cache boundary, the render uses the fresh cache from the pending update.
1372
+ await act(async () => {
1373
+ root.render(
1374
+ <Suspense fallback="Loading...">
1375
+ <Cache key="C">
1376
+ <AsyncText showVersion={true} text="C" />
1377
+ </Cache>
1378
+ </Suspense>,
1379
+ );
1380
+ });
1381
+ expect(Scheduler).toHaveYielded(['Cache miss! [C]']);
1382
+ expect(root).toMatchRenderedOutput('Loading...');
1383
+
1384
+ await act(async () => {
1385
+ resolveMostRecentTextCache('C');
1386
+ });
1387
+ expect(Scheduler).toHaveYielded(['C [v2]']);
1388
+ expect(root).toMatchRenderedOutput('C [v2]');
1389
+
1390
+ // Unmount children: the fresh cache used for the updates is freed, while the
1391
+ // original cache (with A) is still retained at the root.
1392
+ await act(async () => {
1393
+ root.render('Bye!');
1394
+ });
1395
+ expect(Scheduler).toHaveYielded([
1396
+ 'Cache cleanup: B [v2]',
1397
+ 'Cache cleanup: C [v2]',
1398
+ ]);
1399
+ expect(root).toMatchRenderedOutput('Bye!');
1400
+ });
1401
+
1402
+ // @gate experimental || www
1403
+ test('cleans up cache only used in an aborted transition', async () => {
1404
+ const root = ReactNoop.createRoot();
1405
+ seedNextTextCache('A');
1406
+ await act(async () => {
1407
+ root.render(
1408
+ <Suspense fallback="Loading...">
1409
+ <Cache key="A">
1410
+ <AsyncText showVersion={true} text="A" />
1411
+ </Cache>
1412
+ </Suspense>,
1413
+ );
1414
+ });
1415
+ expect(Scheduler).toHaveYielded(['A [v1]']);
1416
+ expect(root).toMatchRenderedOutput('A [v1]');
1417
+
1418
+ // Start a transition from A -> B..., which should create a fresh cache
1419
+ // for the new cache boundary (bc of the different key)
1420
+ await act(async () => {
1421
+ startTransition(() => {
1422
+ root.render(
1423
+ <Suspense fallback="Loading...">
1424
+ <Cache key="B">
1425
+ <AsyncText showVersion={true} text="B" />
1426
+ </Cache>
1427
+ </Suspense>,
1428
+ );
1429
+ });
1430
+ });
1431
+ expect(Scheduler).toHaveYielded(['Cache miss! [B]']);
1432
+ expect(root).toMatchRenderedOutput('A [v1]');
1433
+
1434
+ // ...but cancel by transitioning "back" to A (which we never really left)
1435
+ await act(async () => {
1436
+ startTransition(() => {
1437
+ root.render(
1438
+ <Suspense fallback="Loading...">
1439
+ <Cache key="A">
1440
+ <AsyncText showVersion={true} text="A" />
1441
+ </Cache>
1442
+ </Suspense>,
1443
+ );
1444
+ });
1445
+ });
1446
+ expect(Scheduler).toHaveYielded(['A [v1]', 'Cache cleanup: B [v2]']);
1447
+ expect(root).toMatchRenderedOutput('A [v1]');
1448
+
1449
+ // Unmount children: ...
1450
+ await act(async () => {
1451
+ root.render('Bye!');
1452
+ });
1453
+ expect(Scheduler).toHaveYielded([]);
1454
+ expect(root).toMatchRenderedOutput('Bye!');
1455
+ });
1456
+
1457
+ // @gate experimental || www
1458
+ test.skip('if a root cache refresh never commits its fresh cache is released', async () => {
1459
+ const root = ReactNoop.createRoot();
1460
+ let refresh;
1461
+ function Example({text}) {
1462
+ refresh = useCacheRefresh();
1463
+ return <AsyncText showVersion={true} text={text} />;
1464
+ }
1465
+ seedNextTextCache('A');
1466
+ await act(async () => {
1467
+ root.render(
1468
+ <Suspense fallback="Loading...">
1469
+ <Example text="A" />
1470
+ </Suspense>,
1471
+ );
1472
+ });
1473
+ expect(Scheduler).toHaveYielded(['A [v1]']);
1474
+ expect(root).toMatchRenderedOutput('A [v1]');
1475
+
1476
+ await act(async () => {
1477
+ startTransition(() => {
1478
+ refresh();
1479
+ });
1480
+ });
1481
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]']);
1482
+ expect(root).toMatchRenderedOutput('A [v1]');
1483
+
1484
+ await act(async () => {
1485
+ root.render('Bye!');
1486
+ });
1487
+ expect(Scheduler).toHaveYielded([
1488
+ // TODO: the v1 cache should *not* be cleaned up, it is still retained by the root
1489
+ // The following line is presently yielded but should not be:
1490
+ // 'Cache cleanup: A [v1]',
1491
+
1492
+ // TODO: the v2 cache *should* be cleaned up, it was created for the abandoned refresh
1493
+ // The following line is presently not yielded but should be:
1494
+ 'Cache cleanup: A [v2]',
1495
+ ]);
1496
+ expect(root).toMatchRenderedOutput('Bye!');
1497
+ });
1498
+
1499
+ // @gate experimental || www
1500
+ test.skip('if a cache boundary refresh never commits its fresh cache is released', async () => {
1501
+ const root = ReactNoop.createRoot();
1502
+ let refresh;
1503
+ function Example({text}) {
1504
+ refresh = useCacheRefresh();
1505
+ return <AsyncText showVersion={true} text={text} />;
1506
+ }
1507
+ seedNextTextCache('A');
1508
+ await act(async () => {
1509
+ root.render(
1510
+ <Suspense fallback="Loading...">
1511
+ <Cache>
1512
+ <Example text="A" />
1513
+ </Cache>
1514
+ </Suspense>,
1515
+ );
1516
+ });
1517
+ expect(Scheduler).toHaveYielded(['A [v1]']);
1518
+ expect(root).toMatchRenderedOutput('A [v1]');
1519
+
1520
+ await act(async () => {
1521
+ startTransition(() => {
1522
+ refresh();
1523
+ });
1524
+ });
1525
+ expect(Scheduler).toHaveYielded(['Cache miss! [A]']);
1526
+ expect(root).toMatchRenderedOutput('A [v1]');
1527
+
1528
+ // Unmount the boundary before the refresh can complete
1529
+ await act(async () => {
1530
+ root.render('Bye!');
1531
+ });
1532
+ expect(Scheduler).toHaveYielded([
1533
+ // TODO: the v2 cache *should* be cleaned up, it was created for the abandoned refresh
1534
+ // The following line is presently not yielded but should be:
1535
+ 'Cache cleanup: A [v2]',
1536
+ ]);
1537
+ expect(root).toMatchRenderedOutput('Bye!');
1538
});
1539
});