@samitouri / QOS-React / commits / d8e03de4aa

[react-cache] Remove `cache` as argument to `read` (#13865)

* [react-cache] Remove `cache` as argument to `read` Updated is API is `Resource.read(key)` instead of `Resource.read(cache, key)`. The cache is read from context using `readContext`. This also removes cache invalidation entirely (other than the default LRU mechanism), as well as the ability to have multiple caches. We'll add it back once `Context.write` lands and we can implement it the right way. Since there's now only a single cache (the global one), we don't actually need to use context yet, but I've added a dummy context anyway so the user gets an error if they attempt to read outside the render phase. * nits * Add test for thenables that resolve multiple times

Andrew Clark committed Oct 23, 2018 at 14:38 UTC d8e03de4aa52ca7b0bf0baaa2fcf0e8aaf1f547b
11 files changed +732 -692
packages/react-cache/src/LRU.js new
+142
@@ -0,0 +1,142 @@
1 +/**
2 + * Copyright (c) 2014-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import {unstable_scheduleCallback as scheduleCallback} from 'scheduler';
11 +
12 +type Entry<T> = {|
13 + value: T,
14 + onDelete: () => mixed,
15 + previous: Entry<T>,
16 + next: Entry<T>,
17 +|};
18 +
19 +export function createLRU<T>(limit: number) {
20 + let LIMIT = limit;
21 +
22 + // Circular, doubly-linked list
23 + let first: Entry<T> | null = null;
24 + let size: number = 0;
25 +
26 + let cleanUpIsScheduled: boolean = false;
27 +
28 + function scheduleCleanUp() {
29 + if (cleanUpIsScheduled === false && size > LIMIT) {
30 + // The cache size exceeds the limit. Schedule a callback to delete the
31 + // least recently used entries.
32 + cleanUpIsScheduled = true;
33 + scheduleCallback(cleanUp);
34 + }
35 + }
36 +
37 + function cleanUp() {
38 + cleanUpIsScheduled = false;
39 + deleteLeastRecentlyUsedEntries(LIMIT);
40 + }
41 +
42 + function deleteLeastRecentlyUsedEntries(targetSize: number) {
43 + // Delete entries from the cache, starting from the end of the list.
44 + if (first !== null) {
45 + const resolvedFirst: Entry<T> = (first: any);
46 + let last = resolvedFirst.previous;
47 + while (size > targetSize && last !== null) {
48 + const onDelete = last.onDelete;
49 + const previous = last.previous;
50 + last.onDelete = (null: any);
51 +
52 + // Remove from the list
53 + last.previous = last.next = (null: any);
54 + if (last === first) {
55 + // Reached the head of the list.
56 + first = last = null;
57 + } else {
58 + (first: any).previous = previous;
59 + previous.next = (first: any);
60 + last = previous;
61 + }
62 +
63 + size -= 1;
64 +
65 + // Call the destroy method after removing the entry from the list. If it
66 + // throws, the rest of cache will not be deleted, but it will be in a
67 + // valid state.
68 + onDelete();
69 + }
70 + }
71 + }
72 +
73 + function add(value: T, onDelete: () => mixed): Entry<T> {
74 + const entry = {
75 + value,
76 + onDelete,
77 + next: (null: any),
78 + previous: (null: any),
79 + };
80 + if (first === null) {
81 + entry.previous = entry.next = entry;
82 + first = entry;
83 + } else {
84 + // Append to head
85 + const last = first.previous;
86 + last.next = entry;
87 + entry.previous = last;
88 +
89 + first.previous = entry;
90 + entry.next = first;
91 +
92 + first = entry;
93 + }
94 + size += 1;
95 + return entry;
96 + }
97 +
98 + function update(entry: Entry<T>, newValue: T): void {
99 + entry.value = newValue;
100 + }
101 +
102 + function access(entry: Entry<T>): T {
103 + const next = entry.next;
104 + if (next !== null) {
105 + // Entry already cached
106 + const resolvedFirst: Entry<T> = (first: any);
107 + if (first !== entry) {
108 + // Remove from current position
109 + const previous = entry.previous;
110 + previous.next = next;
111 + next.previous = previous;
112 +
113 + // Append to head
114 + const last = resolvedFirst.previous;
115 + last.next = entry;
116 + entry.previous = last;
117 +
118 + resolvedFirst.previous = entry;
119 + entry.next = resolvedFirst;
120 +
121 + first = entry;
122 + }
123 + } else {
124 + // Cannot access a deleted entry
125 + // TODO: Error? Warning?
126 + }
127 + scheduleCleanUp();
128 + return entry.value;
129 + }
130 +
131 + function setLimit(newLimit: number) {
132 + LIMIT = newLimit;
133 + scheduleCleanUp();
134 + }
135 +
136 + return {
137 + add,
138 + update,
139 + access,
140 + setLimit,
141 + };
142 +}
packages/react-cache/src/ReactCache.js
+131 -347
@@ -10,394 +10,178 @@
10 import React from 'react';
11 import warningWithoutStack from 'shared/warningWithoutStack';
12
13 -function noop() {}
13 +import {createLRU} from './LRU';
14
15 -const Empty = 0;
16 -const Pending = 1;
17 -const Resolved = 2;
18 -const Rejected = 3;
15 +type Thenable<T> = {
16 + then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed,
17 +};
18 +
19 +type Suspender = {
20 + then(resolve: () => mixed, reject: () => mixed): mixed,
21 +};
22
20 -type EmptyRecord<K> = {|
23 +type PendingResult = {|
24 status: 0,
22 - suspender: null,
23 - key: K,
24 - value: null,
25 - error: null,
26 - next: any, // TODO: (issue #12941)
27 - previous: any, // TODO: (issue #12941)
28 - /**
29 - * Proper types would be something like this:
30 - * next: Record<K, V> | null,
31 - * previous: Record<K, V> | null,
32 - */
25 + value: Suspender,
26 |};
27
35 -type PendingRecord<K, V> = {|
28 +type ResolvedResult<V> = {|
29 status: 1,
37 - suspender: Promise<V>,
38 - key: K,
39 - value: null,
40 - error: null,
41 - next: any, // TODO: (issue #12941)
42 - previous: any, // TODO: (issue #12941)
43 - /**
44 - * Proper types would be something like this:
45 - * next: Record<K, V> | null,
46 - * previous: Record<K, V> | null,
47 - */
48 -|};
49 -
50 -type ResolvedRecord<K, V> = {|
51 - status: 2,
52 - suspender: null,
53 - key: K,
30 value: V,
55 - error: null,
56 - next: any, // TODO: (issue #12941)
57 - previous: any, // TODO: (issue #12941)
58 - /**
59 - * Proper types would be something like this:
60 - * next: Record<K, V> | null,
61 - * previous: Record<K, V> | null,
62 - */
63 -|};
64 -
65 -type RejectedRecord<K> = {|
66 - status: 3,
67 - suspender: null,
68 - key: K,
69 - value: null,
70 - error: Error,
71 - next: any, // TODO: (issue #12941)
72 - previous: any, // TODO: (issue #12941)
73 - /**
74 - * Proper types would be something like this:
75 - * next: Record<K, V> | null,
76 - * previous: Record<K, V> | null,
77 - */
31 |};
32
80 -type Record<K, V> =
81 - | EmptyRecord<K>
82 - | PendingRecord<K, V>
83 - | ResolvedRecord<K, V>
84 - | RejectedRecord<K>;
85 -
86 -type RecordCache<K, V> = {|
87 - map: Map<K, Record<K, V>>,
88 - head: Record<K, V> | null,
89 - tail: Record<K, V> | null,
90 - size: number,
33 +type RejectedResult = {|
34 + status: 2,
35 + value: mixed,
36 |};
37
93 -// TODO: How do you express this type with Flow?
94 -type ResourceMap = Map<any, RecordCache<any, any>>;
95 -type Cache = {
96 - invalidate(): void,
97 - read<K, V, A>(
98 - resourceType: mixed,
99 - key: K,
100 - miss: (A) => Promise<V>,
101 - missArg: A,
102 - ): V,
103 - preload<K, V, A>(
104 - resourceType: mixed,
105 - key: K,
106 - miss: (A) => Promise<V>,
107 - missArg: A,
108 - ): void,
38 +type Result<V> = PendingResult | ResolvedResult<V> | RejectedResult;
39
110 - // DEV-only
111 - $$typeof?: Symbol | number,
40 +type Resource<I, V> = {
41 + read(I): V,
42 + preload(I): void,
43 };
44
114 -let CACHE_TYPE;
115 -if (__DEV__) {
116 - CACHE_TYPE = 0xcac4e;
117 -}
118 -
119 -let isCache;
120 -if (__DEV__) {
121 - isCache = value =>
122 - value !== null &&
123 - typeof value === 'object' &&
124 - value.$$typeof === CACHE_TYPE;
125 -}
45 +const Pending = 0;
46 +const Resolved = 1;
47 +const Rejected = 2;
48
127 -// TODO: Make this configurable per resource
128 -const MAX_SIZE = 500;
129 -const PAGE_SIZE = 50;
49 +const currentOwner =
50 + React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner;
51
131 -function createRecord<K>(key: K): EmptyRecord<K> {
132 - return {
133 - status: Empty,
134 - suspender: null,
135 - key,
136 - value: null,
137 - error: null,
138 - next: null,
139 - previous: null,
140 - };
52 +function readContext(Context, observedBits) {
53 + const dispatcher = currentOwner.currentDispatcher;
54 + if (dispatcher === null) {
55 + throw new Error(
56 + 'react-cache: read and preload may only be called from within a ' +
57 + "component's render. They are not supported in event handlers or " +
58 + 'lifecycle methods.',
59 + );
60 + }
61 + return dispatcher.readContext(Context, observedBits);
62 }
63
143 -function createRecordCache<K, V>(): RecordCache<K, V> {
144 - return {
145 - map: new Map(),
146 - head: null,
147 - tail: null,
148 - size: 0,
149 - };
64 +function identityHashFn(input) {
65 + if (__DEV__) {
66 + warningWithoutStack(
67 + typeof input === 'string' ||
68 + typeof input === 'number' ||
69 + typeof input === 'boolean' ||
70 + input === undefined ||
71 + input === null,
72 + 'Invalid key type. Expected a string, number, symbol, or boolean, ' +
73 + 'but instead received: %s' +
74 + '\n\nTo use non-primitive values as keys, you must pass a hash ' +
75 + 'function as the second argument to createResource().',
76 + input,
77 + );
78 + }
79 + return input;
80 }
81
152 -export function createCache(invalidator: () => mixed): Cache {
153 - const resourceMap: ResourceMap = new Map();
82 +const CACHE_LIMIT = 500;
83 +const lru = createLRU(CACHE_LIMIT);
84
155 - function accessRecord<K, V>(resourceType: any, key: K): Record<K, V> {
156 - if (__DEV__) {
157 - warningWithoutStack(
158 - typeof resourceType !== 'string' && typeof resourceType !== 'number',
159 - 'Invalid resourceType: Expected a symbol, object, or function, but ' +
160 - 'instead received: %s. Strings and numbers are not permitted as ' +
161 - 'resource types.',
162 - resourceType,
163 - );
164 - }
85 +const entries: Map<Resource<any, any>, Map<any, any>> = new Map();
86
166 - let recordCache = resourceMap.get(resourceType);
167 - if (recordCache === undefined) {
168 - recordCache = createRecordCache();
169 - resourceMap.set(resourceType, recordCache);
170 - }
171 - const map = recordCache.map;
87 +const CacheContext = React.createContext(null);
88
173 - let record = map.get(key);
174 - if (record === undefined) {
175 - // This record does not already exist. Create a new one.
176 - record = createRecord(key);
177 - map.set(key, record);
178 - if (recordCache.size >= MAX_SIZE) {
179 - // The cache is already at maximum capacity. Remove PAGE_SIZE least
180 - // recently used records.
181 - // TODO: We assume the max capcity is greater than zero. Otherwise warn.
182 - const tail = recordCache.tail;
183 - if (tail !== null) {
184 - let newTail = tail;
185 - for (let i = 0; i < PAGE_SIZE && newTail !== null; i++) {
186 - recordCache.size -= 1;
187 - map.delete(newTail.key);
188 - newTail = newTail.previous;
189 - }
190 - recordCache.tail = newTail;
191 - if (newTail !== null) {
192 - newTail.next = null;
193 - }
194 - }
195 - }
196 - } else {
197 - // This record is already cached. Remove it from its current position in
198 - // the list. We'll add it to the front below.
199 - const previous = record.previous;
200 - const next = record.next;
201 - if (previous !== null) {
202 - previous.next = next;
203 - } else {
204 - recordCache.head = next;
205 - }
206 - if (next !== null) {
207 - next.previous = previous;
208 - } else {
209 - recordCache.tail = previous;
210 - }
211 - recordCache.size -= 1;
212 - }
213 -
214 - // Add the record to the front of the list.
215 - const head = recordCache.head;
216 - const newHead = record;
217 - recordCache.head = newHead;
218 - newHead.previous = null;
219 - newHead.next = head;
220 - if (head !== null) {
221 - head.previous = newHead;
222 - } else {
223 - recordCache.tail = newHead;
224 - }
225 - recordCache.size += 1;
226 -
227 - return newHead;
89 +function accessResult<I, K, V>(
90 + resource: any,
91 + fetch: I => Thenable<V>,
92 + input: I,
93 + key: K,
94 +): Result<V> {
95 + let entriesForResource = entries.get(resource);
96 + if (entriesForResource === undefined) {
97 + entriesForResource = new Map();
98 + entries.set(resource, entriesForResource);
99 }
229 -
230 - function load<K, V>(emptyRecord: EmptyRecord<K>, suspender: Promise<V>) {
231 - const pendingRecord: PendingRecord<K, V> = (emptyRecord: any);
232 - pendingRecord.status = Pending;
233 - pendingRecord.suspender = suspender;
234 - suspender.then(
100 + let entry = entriesForResource.get(key);
101 + if (entry === undefined) {
102 + const thenable = fetch(input);
103 + thenable.then(
104 value => {
236 - // Resource loaded successfully.
237 - const resolvedRecord: ResolvedRecord<K, V> = (pendingRecord: any);
238 - resolvedRecord.status = Resolved;
239 - resolvedRecord.suspender = null;
240 - resolvedRecord.value = value;
105 + if (newResult.status === Pending) {
106 + const resolvedResult: ResolvedResult<V> = (newResult: any);
107 + resolvedResult.status = Resolved;
108 + resolvedResult.value = value;
109 + }
110 },
111 error => {
243 - // Resource failed to load. Stash the error for later so we can throw it
244 - // the next time it's requested.
245 - const rejectedRecord: RejectedRecord<K> = (pendingRecord: any);
246 - rejectedRecord.status = Rejected;
247 - rejectedRecord.suspender = null;
248 - rejectedRecord.error = error;
112 + if (newResult.status === Pending) {
113 + const rejectedResult: RejectedResult = (newResult: any);
114 + rejectedResult.status = Rejected;
115 + rejectedResult.value = error;
116 + }
117 },
118 );
119 + const newResult: PendingResult = {
120 + status: Pending,
121 + value: thenable,
122 + };
123 + const newEntry = lru.add(newResult, deleteEntry.bind(null, resource, key));
124 + entriesForResource.set(key, newEntry);
125 + return newResult;
126 + } else {
127 + return (lru.access(entry): any);
128 }
252 -
253 - const cache: Cache = {
254 - invalidate() {
255 - invalidator();
256 - },
257 - preload<K, V, A>(
258 - resourceType: any,
259 - key: K,
260 - miss: A => Promise<V>,
261 - missArg: A,
262 - ): void {
263 - const record: Record<K, V> = accessRecord(resourceType, key);
264 - switch (record.status) {
265 - case Empty:
266 - // Warm the cache.
267 - const suspender = miss(missArg);
268 - load(record, suspender);
269 - return;
270 - case Pending:
271 - // There's already a pending request.
272 - return;
273 - case Resolved:
274 - // The resource is already in the cache.
275 - return;
276 - case Rejected:
277 - // The request failed.
278 - return;
279 - }
280 - },
281 - read<K, V, A>(
282 - resourceType: any,
283 - key: K,
284 - miss: A => Promise<V>,
285 - missArg: A,
286 - ): V {
287 - const record: Record<K, V> = accessRecord(resourceType, key);
288 - switch (record.status) {
289 - case Empty:
290 - // Load the requested resource.
291 - const suspender = miss(missArg);
292 - load(record, suspender);
293 - throw suspender;
294 - case Pending:
295 - // There's already a pending request.
296 - throw record.suspender;
297 - case Resolved:
298 - return record.value;
299 - case Rejected:
300 - default:
301 - // The requested resource previously failed loading.
302 - const error = record.error;
303 - throw error;
304 - }
305 - },
306 - };
307 -
308 - if (__DEV__) {
309 - cache.$$typeof = CACHE_TYPE;
310 - }
311 - return cache;
129 }
130
314 -let warnIfNonPrimitiveKey;
315 -if (__DEV__) {
316 - warnIfNonPrimitiveKey = (key, methodName) => {
317 - warningWithoutStack(
318 - typeof key === 'string' ||
319 - typeof key === 'number' ||
320 - typeof key === 'boolean' ||
321 - key === undefined ||
322 - key === null,
323 - '%s: Invalid key type. Expected a string, number, symbol, or boolean, ' +
324 - 'but instead received: %s' +
325 - '\n\nTo use non-primitive values as keys, you must pass a hash ' +
326 - 'function as the second argument to unstable_createResource().',
327 - methodName,
328 - key,
329 - );
330 - };
131 +function deleteEntry(resource, key) {
132 + const entriesForResource = entries.get(resource);
133 + if (entriesForResource !== undefined) {
134 + entriesForResource.delete(key);
135 + if (entriesForResource.size === 0) {
136 + entries.delete(resource);
137 + }
138 + }
139 }
140
333 -type primitive = string | number | boolean | void | null;
334 -
335 -type Resource<K, V> = {|
336 - read(Cache, K): V,
337 - preload(cache: Cache, key: K): void,
338 -|};
339 -
340 -// These declarations are used to express function overloading. I wish there
341 -// were a more elegant way to do this in the function definition itself.
342 -
343 -// Primitive keys do not request a hash function.
344 -declare function unstable_createResource<V, K: primitive, H: primitive>(
345 - loadResource: (K) => Promise<V>,
346 - hash?: (K) => H,
347 -): Resource<K, V>;
348 -
349 -// Non-primitive keys *do* require a hash function.
350 -// eslint-disable-next-line no-redeclare
351 -declare function unstable_createResource<V, K: mixed, H: primitive>(
352 - loadResource: (K) => Promise<V>,
353 - hash: (K) => H,
354 -): Resource<K, V>;
141 +export function unstable_createResource<I, K: string | number, V>(
142 + fetch: I => Thenable<V>,
143 + maybeHashInput?: I => K,
144 +): Resource<I, V> {
145 + const hashInput: I => K =
146 + maybeHashInput !== undefined ? maybeHashInput : (identityHashFn: any);
147
356 -// eslint-disable-next-line no-redeclare
357 -export function unstable_createResource<V, K, H: primitive>(
358 - loadResource: K => Promise<V>,
359 - hash: K => H,
360 -): Resource<K, V> {
148 const resource = {
362 - read(cache, key) {
363 - if (__DEV__) {
364 - warningWithoutStack(
365 - isCache(cache),
366 - 'read(): The first argument must be a cache. Instead received: %s',
367 - cache,
368 - );
369 - }
370 - if (hash === undefined) {
371 - if (__DEV__) {
372 - warnIfNonPrimitiveKey(key, 'read');
149 + read(input: I): V {
150 + // react-cache currently doesn't rely on context, but it may in the
151 + // future, so we read anyway to prevent access outside of render.
152 + readContext(CacheContext);
153 + const key = hashInput(input);
154 + const result: Result<V> = accessResult(resource, fetch, input, key);
155 + switch (result.status) {
156 + case Pending: {
157 + const suspender = result.value;
158 + throw suspender;
159 }
374 - return cache.read(resource, key, loadResource, key);
375 - }
376 - const hashedKey = hash(key);
377 - return cache.read(resource, hashedKey, loadResource, key);
378 - },
379 - preload(cache, key) {
380 - if (__DEV__) {
381 - warningWithoutStack(
382 - isCache(cache),
383 - 'preload(): The first argument must be a cache. Instead received: %s',
384 - cache,
385 - );
386 - }
387 - if (hash === undefined) {
388 - if (__DEV__) {
389 - warnIfNonPrimitiveKey(key, 'preload');
160 + case Resolved: {
161 + const value = result.value;
162 + return value;
163 + }
164 + case Rejected: {
165 + const error = result.value;
166 + throw error;
167 }
391 - cache.preload(resource, key, loadResource, key);
392 - return;
168 + default:
169 + // Should be unreachable
170 + return (undefined: any);
171 }
394 - const hashedKey = hash(key);
395 - cache.preload(resource, hashedKey, loadResource, key);
172 + },
173 +
174 + preload(input: I): void {
175 + // react-cache currently doesn't rely on context, but it may in the
176 + // future, so we read anyway to prevent access outside of render.
177 + readContext(CacheContext);
178 + const key = hashInput(input);
179 + accessResult(resource, fetch, input, key);
180 },
181 };
182 return resource;
183 }
184
401 -// Global cache has no eviction policy (except for, ya know, a browser refresh).
402 -const globalCache = createCache(noop);
403 -export const ReactCache = React.createContext(globalCache);
185 +export function unstable_setGlobalCacheLimit(limit: number) {
186 + lru.setLimit(limit);
187 +}
packages/react-cache/src/__tests__/ReactCache-test.internal.js new
+402
@@ -0,0 +1,402 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +let ReactCache;
13 +let createResource;
14 +let React;
15 +let ReactFeatureFlags;
16 +let ReactTestRenderer;
17 +let Suspense;
18 +let TextResource;
19 +let textResourceShouldFail;
20 +let flushScheduledWork;
21 +let evictLRU;
22 +
23 +describe('ReactCache', () => {
24 + beforeEach(() => {
25 + jest.resetModules();
26 +
27 + jest.mock('scheduler', () => {
28 + let callbacks = [];
29 + return {
30 + unstable_scheduleCallback(callback) {
31 + const callbackIndex = callbacks.length;
32 + callbacks.push(callback);
33 + return {callbackIndex};
34 + },
35 + flushScheduledWork() {
36 + while (callbacks.length) {
37 + const callback = callbacks.pop();
38 + callback();
39 + }
40 + },
41 + };
42 + });
43 +
44 + ReactFeatureFlags = require('shared/ReactFeatureFlags');
45 + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
46 + ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
47 + React = require('react');
48 + Suspense = React.Suspense;
49 + ReactCache = require('react-cache');
50 + createResource = ReactCache.unstable_createResource;
51 + ReactTestRenderer = require('react-test-renderer');
52 + flushScheduledWork = require('scheduler').flushScheduledWork;
53 + evictLRU = flushScheduledWork;
54 +
55 + TextResource = createResource(([text, ms = 0]) => {
56 + let listeners = null;
57 + let status = 'pending';
58 + let value = null;
59 + return {
60 + then(resolve, reject) {
61 + switch (status) {
62 + case 'pending': {
63 + if (listeners === null) {
64 + listeners = [{resolve, reject}];
65 + setTimeout(() => {
66 + if (textResourceShouldFail) {
67 + ReactTestRenderer.unstable_yield(
68 + `Promise rejected [${text}]`,
69 + );
70 + status = 'rejected';
71 + value = new Error('Failed to load: ' + text);
72 + listeners.forEach(listener => listener.reject(value));
73 + } else {
74 + ReactTestRenderer.unstable_yield(
75 + `Promise resolved [${text}]`,
76 + );
77 + status = 'resolved';
78 + value = text;
79 + listeners.forEach(listener => listener.resolve(value));
80 + }
81 + }, ms);
82 + } else {
83 + listeners.push({resolve, reject});
84 + }
85 + break;
86 + }
87 + case 'resolved': {
88 + resolve(value);
89 + break;
90 + }
91 + case 'rejected': {
92 + reject(value);
93 + break;
94 + }
95 + }
96 + },
97 + };
98 + }, ([text, ms]) => text);
99 +
100 + textResourceShouldFail = false;
101 + });
102 +
103 + function Text(props) {
104 + ReactTestRenderer.unstable_yield(props.text);
105 + return props.text;
106 + }
107 +
108 + function AsyncText(props) {
109 + const text = props.text;
110 + try {
111 + TextResource.read([props.text, props.ms]);
112 + ReactTestRenderer.unstable_yield(text);
113 + return text;
114 + } catch (promise) {
115 + if (typeof promise.then === 'function') {
116 + ReactTestRenderer.unstable_yield(`Suspend! [${text}]`);
117 + } else {
118 + ReactTestRenderer.unstable_yield(`Error! [${text}]`);
119 + }
120 + throw promise;
121 + }
122 + }
123 +
124 + it('throws a promise if the requested value is not in the cache', () => {
125 + function App() {
126 + return (
127 + <Suspense fallback={<Text text="Loading..." />}>
128 + <AsyncText ms={100} text="Hi" />
129 + </Suspense>
130 + );
131 + }
132 +
133 + const root = ReactTestRenderer.create(<App />, {
134 + unstable_isConcurrent: true,
135 + });
136 +
137 + expect(root).toFlushAndYield(['Suspend! [Hi]', 'Loading...']);
138 +
139 + jest.advanceTimersByTime(100);
140 + expect(ReactTestRenderer).toHaveYielded(['Promise resolved [Hi]']);
141 + expect(root).toFlushAndYield(['Hi']);
142 + });
143 +
144 + it('throws an error on the subsequent read if the promise is rejected', async () => {
145 + function App() {
146 + return (
147 + <Suspense fallback={<Text text="Loading..." />}>
148 + <AsyncText ms={100} text="Hi" />
149 + </Suspense>
150 + );
151 + }
152 +
153 + const root = ReactTestRenderer.create(<App />, {
154 + unstable_isConcurrent: true,
155 + });
156 +
157 + expect(root).toFlushAndYield(['Suspend! [Hi]', 'Loading...']);
158 +
159 + textResourceShouldFail = true;
160 + jest.advanceTimersByTime(100);
161 + expect(ReactTestRenderer).toHaveYielded(['Promise rejected [Hi]']);
162 +
163 + expect(root).toFlushAndThrow('Failed to load: Hi');
164 + expect(ReactTestRenderer).toHaveYielded(['Error! [Hi]', 'Error! [Hi]']);
165 +
166 + // Should throw again on a subsequent read
167 + root.update(<App />);
168 + expect(root).toFlushAndThrow('Failed to load: Hi');
169 + expect(ReactTestRenderer).toHaveYielded(['Error! [Hi]', 'Error! [Hi]']);
170 + });
171 +
172 + it('warns if non-primitive key is passed to a resource without a hash function', () => {
173 + const BadTextResource = createResource(([text, ms = 0]) => {
174 + return new Promise((resolve, reject) =>
175 + setTimeout(() => {
176 + resolve(text);
177 + }, ms),
178 + );
179 + });
180 +
181 + function App() {
182 + ReactTestRenderer.unstable_yield('App');
183 + return BadTextResource.read(['Hi', 100]);
184 + }
185 +
186 + const root = ReactTestRenderer.create(
187 + <Suspense fallback={<Text text="Loading..." />}>
188 + <App />
189 + </Suspense>,
190 + {
191 + unstable_isConcurrent: true,
192 + },
193 + );
194 +
195 + if (__DEV__) {
196 + expect(() => {
197 + expect(root).toFlushAndYield(['App', 'Loading...']);
198 + }).toWarnDev(
199 + [
200 + 'Invalid key type. Expected a string, number, symbol, or ' +
201 + 'boolean, but instead received: Hi,100\n\n' +
202 + 'To use non-primitive values as keys, you must pass a hash ' +
203 + 'function as the second argument to createResource().',
204 + ],
205 + {withoutStack: true},
206 + );
207 + } else {
208 + expect(root).toFlushAndYield(['App', 'Loading...']);
209 + }
210 + });
211 +
212 + it('evicts least recently used values', async () => {
213 + ReactCache.unstable_setGlobalCacheLimit(3);
214 +
215 + // Render 1, 2, and 3
216 + const root = ReactTestRenderer.create(
217 + <Suspense fallback={<Text text="Loading..." />}>
218 + <AsyncText ms={100} text={1} />
219 + <AsyncText ms={100} text={2} />
220 + <AsyncText ms={100} text={3} />
221 + </Suspense>,
222 + {
223 + unstable_isConcurrent: true,
224 + },
225 + );
226 + expect(root).toFlushAndYield([
227 + 'Suspend! [1]',
228 + 'Suspend! [2]',
229 + 'Suspend! [3]',
230 + 'Loading...',
231 + ]);
232 + jest.advanceTimersByTime(100);
233 + expect(ReactTestRenderer).toHaveYielded([
234 + 'Promise resolved [1]',
235 + 'Promise resolved [2]',
236 + 'Promise resolved [3]',
237 + ]);
238 + expect(root).toFlushAndYield([1, 2, 3]);
239 + expect(root).toMatchRenderedOutput('123');
240 +
241 + // Render 1, 4, 5
242 + root.update(
243 + <Suspense fallback={<Text text="Loading..." />}>
244 + <AsyncText ms={100} text={1} />
245 + <AsyncText ms={100} text={4} />
246 + <AsyncText ms={100} text={5} />
247 + </Suspense>,
248 + );
249 +
250 + expect(root).toFlushAndYield([
251 + 1,
252 + 'Suspend! [4]',
253 + 'Suspend! [5]',
254 + 'Loading...',
255 + ]);
256 + jest.advanceTimersByTime(100);
257 + expect(ReactTestRenderer).toHaveYielded([
258 + 'Promise resolved [4]',
259 + 'Promise resolved [5]',
260 + ]);
261 + expect(root).toFlushAndYield([1, 4, 5]);
262 + expect(root).toMatchRenderedOutput('145');
263 +
264 + // We've now rendered values 1, 2, 3, 4, 5, over our limit of 3. The least
265 + // recently used values are 2 and 3. They will be evicted during the
266 + // next sweep.
267 + evictLRU();
268 +
269 + root.update(
270 + <Suspense fallback={<Text text="Loading..." />}>
271 + <AsyncText ms={100} text={1} />
272 + <AsyncText ms={100} text={2} />
273 + <AsyncText ms={100} text={3} />
274 + </Suspense>,
275 + );
276 +
277 + expect(root).toFlushAndYield([
278 + // 1 is still cached
279 + 1,
280 + // 2 and 3 suspend because they were evicted from the cache
281 + 'Suspend! [2]',
282 + 'Suspend! [3]',
283 + 'Loading...',
284 + ]);
285 + jest.advanceTimersByTime(100);
286 + expect(ReactTestRenderer).toHaveYielded([
287 + 'Promise resolved [2]',
288 + 'Promise resolved [3]',
289 + ]);
290 + expect(root).toFlushAndYield([1, 2, 3]);
291 + expect(root).toMatchRenderedOutput('123');
292 + });
293 +
294 + it('preloads during the render phase', async () => {
295 + function App() {
296 + TextResource.preload(['B', 1000]);
297 + TextResource.read(['A', 1000]);
298 + TextResource.read(['B', 1000]);
299 + return <Text text="Result" />;
300 + }
301 +
302 + const root = ReactTestRenderer.create(
303 + <Suspense fallback={<Text text="Loading..." />}>
304 + <App />
305 + </Suspense>,
306 + {
307 + unstable_isConcurrent: true,
308 + },
309 + );
310 +
311 + expect(root).toFlushAndYield(['Loading...']);
312 +
313 + jest.advanceTimersByTime(1000);
314 + expect(ReactTestRenderer).toHaveYielded([
315 + 'Promise resolved [B]',
316 + 'Promise resolved [A]',
317 + ]);
318 + expect(root).toFlushAndYield(['Result']);
319 + expect(root).toMatchRenderedOutput('Result');
320 + });
321 +
322 + it('if a thenable resolves multiple times, does not update the first cached value', () => {
323 + let resolveThenable;
324 + const BadTextResource = createResource(([text, ms = 0]) => {
325 + let listeners = null;
326 + let value = null;
327 + return {
328 + then(resolve, reject) {
329 + if (value !== null) {
330 + resolve(value);
331 + } else {
332 + if (listeners === null) {
333 + listeners = [resolve];
334 + resolveThenable = v => {
335 + listeners.forEach(listener => listener(v));
336 + };
337 + } else {
338 + listeners.push(resolve);
339 + }
340 + }
341 + },
342 + };
343 + }, ([text, ms]) => text);
344 +
345 + function BadAsyncText(props) {
346 + const text = props.text;
347 + try {
348 + const actualText = BadTextResource.read([props.text, props.ms]);
349 + ReactTestRenderer.unstable_yield(actualText);
350 + return actualText;
351 + } catch (promise) {
352 + if (typeof promise.then === 'function') {
353 + ReactTestRenderer.unstable_yield(`Suspend! [${text}]`);
354 + } else {
355 + ReactTestRenderer.unstable_yield(`Error! [${text}]`);
356 + }
357 + throw promise;
358 + }
359 + }
360 +
361 + const root = ReactTestRenderer.create(
362 + <Suspense fallback={<Text text="Loading..." />}>
363 + <BadAsyncText text="Hi" />
364 + </Suspense>,
365 + {
366 + unstable_isConcurrent: true,
367 + },
368 + );
369 +
370 + expect(root).toFlushAndYield(['Suspend! [Hi]', 'Loading...']);
371 +
372 + resolveThenable('Hi');
373 + // This thenable improperly resolves twice. We should not update the
374 + // cached value.
375 + resolveThenable('Hi muahahaha I am different');
376 +
377 + root.update(
378 + <Suspense fallback={<Text text="Loading..." />}>
379 + <BadAsyncText text="Hi" />
380 + </Suspense>,
381 + {
382 + unstable_isConcurrent: true,
383 + },
384 + );
385 +
386 + expect(ReactTestRenderer).toHaveYielded([]);
387 + expect(root).toFlushAndYield(['Hi']);
388 + expect(root).toMatchRenderedOutput('Hi');
389 + });
390 +
391 + it('throws if read is called outside render', () => {
392 + expect(() => TextResource.read(['A', 1000])).toThrow(
393 + "read and preload may only be called from within a component's render",
394 + );
395 + });
396 +
397 + it('throws if preload is called outside render', () => {
398 + expect(() => TextResource.preload(['A', 1000])).toThrow(
399 + "read and preload may only be called from within a component's render",
400 + );
401 + });
402 +});
packages/react-cache/src/__tests__/ReactCache-test.js deleted
-235
@@ -1,235 +0,0 @@
1 -/**
2 - * Copyright (c) Facebook, Inc. and its affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @emails react-core
8 - */
9 -
10 -'use strict';
11 -
12 -let ReactCache;
13 -
14 -describe('ReactCache', () => {
15 - beforeEach(() => {
16 - jest.resetModules();
17 - ReactCache = require('react-cache');
18 - });
19 -
20 - it('throws a promise if the requested value is not in the cache', async () => {
21 - const {createCache, unstable_createResource} = ReactCache;
22 -
23 - function loadUpperCase(text) {
24 - return Promise.resolve(text.toUpperCase());
25 - }
26 - const UpperCase = unstable_createResource(loadUpperCase);
27 - const cache = createCache();
28 -
29 - let suspender;
30 - try {
31 - UpperCase.read(cache, 'hello');
32 - } catch (v) {
33 - suspender = v;
34 - }
35 -
36 - await suspender;
37 - const result = UpperCase.read(cache, 'hello');
38 - expect(result).toBe('HELLO');
39 - });
40 -
41 - it('throws an error on the subsequent read if the promise is rejected', async () => {
42 - const {createCache, unstable_createResource} = ReactCache;
43 -
44 - let shouldFail = true;
45 - function loadUpperCase(text) {
46 - if (shouldFail) {
47 - // Rejects on the first try
48 - shouldFail = false;
49 - return Promise.reject(new Error('oh no'));
50 - } else {
51 - // Succeeds the second time
52 - return Promise.resolve(text.toUpperCase());
53 - }
54 - }
55 - const UpperCase = unstable_createResource(loadUpperCase);
56 - const cache = createCache();
57 -
58 - let suspender;
59 - try {
60 - UpperCase.read(cache, 'hello');
61 - } catch (v) {
62 - suspender = v;
63 - }
64 -
65 - let error;
66 - try {
67 - await suspender;
68 - } catch (e) {
69 - error = e;
70 - }
71 - expect(() => UpperCase.read(cache, 'hello')).toThrow(error);
72 - expect(error.message).toBe('oh no');
73 -
74 - // On a subsequent read, it should still throw.
75 - try {
76 - UpperCase.read(cache, 'hello');
77 - } catch (v) {
78 - suspender = v;
79 - }
80 - await suspender;
81 - expect(() => UpperCase.read(cache, 'hello')).toThrow(error);
82 - expect(error.message).toBe('oh no');
83 - });
84 -
85 - it('can preload data ahead of time', async () => {
86 - const {createCache, unstable_createResource} = ReactCache;
87 -
88 - function loadUpperCase(text) {
89 - return Promise.resolve(text.toUpperCase());
90 - }
91 - const UpperCase = unstable_createResource(loadUpperCase);
92 - const cache = createCache();
93 -
94 - UpperCase.preload(cache, 'hello');
95 - // Wait for next tick
96 - await Promise.resolve();
97 - const result = UpperCase.read(cache, 'hello');
98 - expect(result).toBe('HELLO');
99 - });
100 -
101 - it('does not throw if preloaded promise rejects', async () => {
102 - const {createCache, unstable_createResource} = ReactCache;
103 -
104 - function loadUpperCase(text) {
105 - return Promise.reject(new Error('uh oh'));
106 - }
107 - const UpperCase = unstable_createResource(loadUpperCase);
108 - const cache = createCache();
109 -
110 - UpperCase.preload(cache, 'hello');
111 - // Wait for next tick
112 - await Promise.resolve();
113 -
114 - expect(() => UpperCase.read(cache, 'hello')).toThrow('uh oh');
115 - });
116 -
117 - it('accepts custom hash function', async () => {
118 - const {createCache, unstable_createResource} = ReactCache;
119 -
120 - function loadSum([a, b]) {
121 - return Promise.resolve(a + b);
122 - }
123 - function hash([a, b]) {
124 - return `${a + b}`;
125 - }
126 - const Sum = unstable_createResource(loadSum, hash);
127 - const cache = createCache();
128 -
129 - Sum.preload(cache, [5, 5]);
130 - Sum.preload(cache, [1, 2]);
131 - await Promise.resolve();
132 -
133 - expect(Sum.read(cache, [5, 5])).toEqual(10);
134 - expect(Sum.read(cache, [1, 2])).toEqual(3);
135 - // The fact that the next line returns synchronously and doesn't throw, even
136 - // though [3, 7] was not preloaded, proves that the hashing function works.
137 - expect(Sum.read(cache, [3, 7])).toEqual(10);
138 - });
139 -
140 - it('warns if resourceType is a string or number', () => {
141 - const {createCache} = ReactCache;
142 -
143 - spyOnDev(console, 'error');
144 - const cache = createCache();
145 -
146 - function fn() {
147 - cache.preload('foo', 'uppercaseA', () => Promise.resolve('A'));
148 - cache.preload(123, 'productOf9And2', () => Promise.resolve(18));
149 - }
150 -
151 - if (__DEV__) {
152 - expect(fn).toWarnDev(
153 - [
154 - 'Invalid resourceType: Expected a symbol, object, or function, but ' +
155 - 'instead received: foo. Strings and numbers are not permitted as ' +
156 - 'resource types.',
157 - 'Invalid resourceType: Expected a symbol, object, or function, but ' +
158 - 'instead received: 123. Strings and numbers are not permitted as ' +
159 - 'resource types.',
160 - ],
161 - {withoutStack: true},
162 - );
163 - } else {
164 - fn();
165 - }
166 - });
167 -
168 - it('warns if non-primitive key is passed to a resource without a hash function', () => {
169 - const {createCache, unstable_createResource} = ReactCache;
170 -
171 - spyOnDev(console, 'error');
172 -
173 - function loadSum([a, b]) {
174 - return Promise.resolve(a + b);
175 - }
176 -
177 - const Sum = unstable_createResource(loadSum);
178 - const cache = createCache();
179 -
180 - function fn() {
181 - Sum.preload(cache, [5, 5]);
182 - }
183 -
184 - if (__DEV__) {
185 - expect(fn).toWarnDev(
186 - [
187 - 'preload: Invalid key type. Expected a string, number, symbol, or ' +
188 - 'boolean, but instead received: 5,5\n\n' +
189 - 'To use non-primitive values as keys, you must pass a hash ' +
190 - 'function as the second argument to unstable_createResource().',
191 - ],
192 - {withoutStack: true},
193 - );
194 - } else {
195 - fn();
196 - }
197 - });
198 -
199 - it('stays within maximum capacity by evicting the least recently used record', async () => {
200 - const {createCache, unstable_createResource} = ReactCache;
201 -
202 - function loadIntegerString(int) {
203 - return Promise.resolve(int + '');
204 - }
205 - const IntegerStringResource = unstable_createResource(loadIntegerString);
206 - const cache = createCache();
207 -
208 - // TODO: This is hard-coded to a maximum size of 500. Make this configurable
209 - // per resource.
210 - for (let n = 1; n <= 500; n++) {
211 - IntegerStringResource.preload(cache, n);
212 - }
213 -
214 - // Access 1, 2, and 3 again. The least recently used integer is now 4.
215 - IntegerStringResource.preload(cache, 3);
216 - IntegerStringResource.preload(cache, 2);
217 - IntegerStringResource.preload(cache, 1);
218 -
219 - // Evict older integers from the cache by adding new ones.
220 - IntegerStringResource.preload(cache, 501);
221 - IntegerStringResource.preload(cache, 502);
222 - IntegerStringResource.preload(cache, 503);
223 -
224 - await Promise.resolve();
225 -
226 - // 1, 2, and 3 should be in the cache. 4, 5, and 6 should have been evicted.
227 - expect(IntegerStringResource.read(cache, 1)).toEqual('1');
228 - expect(IntegerStringResource.read(cache, 2)).toEqual('2');
229 - expect(IntegerStringResource.read(cache, 3)).toEqual('3');
230 -
231 - expect(() => IntegerStringResource.read(cache, 4)).toThrow(Promise);
232 - expect(() => IntegerStringResource.read(cache, 5)).toThrow(Promise);
233 - expect(() => IntegerStringResource.read(cache, 6)).toThrow(Promise);
234 - });
235 -});
packages/react-dom/src/__tests__/ReactDOMSuspensePlaceholder-test.internal.js
+1 -6
@@ -13,7 +13,6 @@ let React;
13 let ReactDOM;
14 let Suspense;
15 let ReactCache;
16 -let cache;
16 let TextResource;
17
18 describe('ReactDOMSuspensePlaceholder', () => {
@@ -27,10 +26,6 @@ describe('ReactDOMSuspensePlaceholder', () => {
26 Suspense = React.Suspense;
27 container = document.createElement('div');
28
30 - function invalidateCache() {
31 - cache = ReactCache.createCache(invalidateCache);
32 - }
33 - invalidateCache();
29 TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
30 return new Promise((resolve, reject) =>
31 setTimeout(() => {
@@ -59,7 +54,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
54
55 function AsyncText(props) {
56 const text = props.text;
62 - TextResource.read(cache, [props.text, props.ms]);
57 + TextResource.read([props.text, props.ms]);
58 return text;
59 }
60
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+3 -8
@@ -6,7 +6,6 @@ let Suspense;
6
7 // let JestReact;
8
9 -let cache;
9 let TextResource;
10 let textResourceShouldFail;
11
@@ -25,10 +24,6 @@ describe('ReactSuspense', () => {
24
25 Suspense = React.Suspense;
26
28 - function invalidateCache() {
29 - cache = ReactCache.createCache(invalidateCache);
30 - }
31 - invalidateCache();
27 TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
28 let listeners = null;
29 let status = 'pending';
@@ -84,7 +79,7 @@ describe('ReactSuspense', () => {
79 function AsyncText(props) {
80 const text = props.text;
81 try {
87 - TextResource.read(cache, [props.text, props.ms]);
82 + TextResource.read([props.text, props.ms]);
83 ReactTestRenderer.unstable_yield(text);
84 return text;
85 } catch (promise) {
@@ -362,7 +357,7 @@ describe('ReactSuspense', () => {
357 const text = `${this.props.text}:${this.state.step}`;
358 const ms = this.props.ms;
359 try {
365 - TextResource.read(cache, [text, ms]);
360 + TextResource.read([text, ms]);
361 ReactTestRenderer.unstable_yield(text);
362 return text;
363 } catch (promise) {
@@ -501,7 +496,7 @@ describe('ReactSuspense', () => {
496 const text = this.props.text;
497 const ms = this.props.ms;
498 try {
504 - TextResource.read(cache, [text, ms]);
499 + TextResource.read([text, ms]);
500 ReactTestRenderer.unstable_yield(text);
501 return text;
502 } catch (promise) {
packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js
+1 -8
@@ -21,8 +21,6 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
21 let ReactFeatureFlags;
22 let ReactCache;
23 let Suspense;
24 -
25 - let cache;
24 let TextResource;
25 let textResourceShouldFail;
26
@@ -34,15 +32,10 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
32 ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
33 React = require('react');
34 ReactTestRenderer = require('react-test-renderer');
37 - // JestReact = require('jest-react');
35 ReactCache = require('react-cache');
36
37 Suspense = React.Suspense;
38
42 - function invalidateCache() {
43 - cache = ReactCache.createCache(invalidateCache);
44 - }
45 - invalidateCache();
39 TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
40 let listeners = null;
41 let status = 'pending';
@@ -98,7 +91,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
91 function AsyncText(props) {
92 const text = props.text;
93 try {
101 - TextResource.read(cache, [props.text, props.ms]);
94 + TextResource.read([props.text, props.ms]);
95 ReactTestRenderer.unstable_yield(text);
96 return text;
97 } catch (promise) {
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+3 -28
@@ -7,7 +7,6 @@ let Suspense;
7 let StrictMode;
8 let ConcurrentMode;
9
10 -let cache;
10 let TextResource;
11 let textResourceShouldFail;
12
@@ -28,10 +27,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
27 StrictMode = React.StrictMode;
28 ConcurrentMode = React.unstable_ConcurrentMode;
29
31 - function invalidateCache() {
32 - cache = ReactCache.createCache(invalidateCache);
33 - }
34 - invalidateCache();
30 TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
31 return new Promise((resolve, reject) =>
32 setTimeout(() => {
@@ -80,7 +75,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
75 function AsyncText(props) {
76 const text = props.text;
77 try {
83 - TextResource.read(cache, [props.text, props.ms]);
78 + TextResource.read([props.text, props.ms]);
79 ReactNoop.yield(text);
80 return <span prop={text} />;
81 } catch (promise) {
@@ -278,16 +273,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
273 expect(ReactNoop.getChildren()).toEqual([
274 span('Caught error: Failed to load: Result'),
275 ]);
281 -
282 - // Reset the error boundary and cache, and try again.
283 - errorBoundary.current.reset();
284 - cache.invalidate();
285 -
286 - expect(ReactNoop.flush()).toEqual(['Suspend! [Result]', 'Loading...']);
287 - ReactNoop.expire(1000);
288 - await advanceTimers(1000);
289 - expect(ReactNoop.flush()).toEqual(['Promise resolved [Result]', 'Result']);
290 - expect(ReactNoop.getChildren()).toEqual([span('Result')]);
276 });
277
278 it('retries on error after falling back to a placeholder', async () => {
@@ -346,16 +331,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
331 expect(ReactNoop.getChildren()).toEqual([
332 span('Caught error: Failed to load: Result'),
333 ]);
349 -
350 - // Reset the error boundary and cache, and try again.
351 - errorBoundary.current.reset();
352 - cache.invalidate();
353 -
354 - expect(ReactNoop.flush()).toEqual(['Suspend! [Result]', 'Loading...']);
355 - ReactNoop.expire(3000);
356 - await advanceTimers(3000);
357 - expect(ReactNoop.flush()).toEqual(['Promise resolved [Result]', 'Result']);
358 - expect(ReactNoop.getChildren()).toEqual([span('Result')]);
334 });
335
336 it('can update at a higher priority while in a suspended state', async () => {
@@ -1339,7 +1314,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1314 const text = props.text;
1315 ReactNoop.yield('constructor');
1316 try {
1342 - TextResource.read(cache, [props.text, props.ms]);
1317 + TextResource.read([props.text, props.ms]);
1318 this.state = {text};
1319 } catch (promise) {
1320 if (typeof promise.then === 'function') {
@@ -1444,7 +1419,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1419 const text = this.props.text;
1420 const ms = this.props.ms;
1421 try {
1447 - TextResource.read(cache, [text, ms]);
1422 + TextResource.read([text, ms]);
1423 ReactNoop.yield(text);
1424 return <span prop={text} />;
1425 } catch (promise) {
packages/react/src/__tests__/ReactProfiler-test.internal.js
+47 -55
@@ -13,11 +13,16 @@
13 let React;
14 let ReactFeatureFlags;
15 let ReactNoop;
16 +let ReactCache;
17 let ReactTestRenderer;
18 let advanceTimeBy;
19 let SchedulerTracing;
20 let mockNow;
21 let AdvanceTime;
22 +let AsyncText;
23 +let Text;
24 +let TextResource;
25 +let resourcePromise;
26
27 function loadModules({
28 enableProfilerTimer = true,
@@ -40,6 +45,7 @@ function loadModules({
45
46 React = require('react');
47 SchedulerTracing = require('scheduler/tracing');
48 + ReactCache = require('react-cache');
49
50 if (useNoopRenderer) {
51 ReactNoop = require('react-noop-renderer');
@@ -68,6 +74,46 @@ function loadModules({
74 return this.props.children || null;
75 }
76 };
77 +
78 + resourcePromise = null;
79 +
80 + function yieldForRenderer(value) {
81 + if (ReactNoop) {
82 + ReactNoop.yield(value);
83 + } else {
84 + ReactTestRenderer.unstable_yield(value);
85 + }
86 + }
87 +
88 + TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
89 + resourcePromise = new Promise((resolve, reject) =>
90 + setTimeout(() => {
91 + yieldForRenderer(`Promise resolved [${text}]`);
92 + resolve(text);
93 + }, ms),
94 + );
95 + return resourcePromise;
96 + }, ([text, ms]) => text);
97 +
98 + AsyncText = ({ms, text}) => {
99 + try {
100 + TextResource.read([text, ms]);
101 + yieldForRenderer(`AsyncText [${text}]`);
102 + return text;
103 + } catch (promise) {
104 + if (typeof promise.then === 'function') {
105 + yieldForRenderer(`Suspend [${text}]`);
106 + } else {
107 + yieldForRenderer(`Error [${text}]`);
108 + }
109 + throw promise;
110 + }
111 + };
112 +
113 + Text = ({text}) => {
114 + yieldForRenderer(`Text [${text}]`);
115 + return text;
116 + };
117 }
118
119 const mockDevToolsForTest = () => {
@@ -2145,12 +2191,6 @@ describe('Profiler', () => {
2191 });
2192
2193 describe('suspense', () => {
2148 - let AsyncText;
2149 - let Text;
2150 - let TextResource;
2151 - let cache;
2152 - let resourcePromise;
2153 -
2194 function awaitableAdvanceTimers(ms) {
2195 jest.advanceTimersByTime(ms);
2196 // Wait until the end of the current tick
@@ -2159,54 +2199,6 @@ describe('Profiler', () => {
2199 });
2200 }
2201
2162 - function yieldForRenderer(value) {
2163 - if (ReactNoop) {
2164 - ReactNoop.yield(value);
2165 - } else {
2166 - ReactTestRenderer.unstable_yield(value);
2167 - }
2168 - }
2169 -
2170 - beforeEach(() => {
2171 - const ReactCache = require('react-cache');
2172 - function invalidateCache() {
2173 - cache = ReactCache.createCache(invalidateCache);
2174 - }
2175 - invalidateCache();
2176 -
2177 - resourcePromise = null;
2178 -
2179 - TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
2180 - resourcePromise = new Promise((resolve, reject) =>
2181 - setTimeout(() => {
2182 - yieldForRenderer(`Promise resolved [${text}]`);
2183 - resolve(text);
2184 - }, ms),
2185 - );
2186 - return resourcePromise;
2187 - }, ([text, ms]) => text);
2188 -
2189 - AsyncText = ({ms, text}) => {
2190 - try {
2191 - TextResource.read(cache, [text, ms]);
2192 - yieldForRenderer(`AsyncText [${text}]`);
2193 - return text;
2194 - } catch (promise) {
2195 - if (typeof promise.then === 'function') {
2196 - yieldForRenderer(`Suspend [${text}]`);
2197 - } else {
2198 - yieldForRenderer(`Error [${text}]`);
2199 - }
2200 - throw promise;
2201 - }
2202 - };
2203 -
2204 - Text = ({text}) => {
2205 - yieldForRenderer(`Text [${text}]`);
2206 - return text;
2207 - };
2208 - });
2209 -
2202 it('traces both the temporary placeholder and the finished render for an interaction', async () => {
2203 loadModulesForTracing({useNoopRenderer: true});
2204
@@ -2352,7 +2344,7 @@ describe('Profiler', () => {
2344
2345 render() {
2346 const {ms, text} = this.props;
2355 - TextResource.read(cache, [text, ms]);
2347 + TextResource.read([text, ms]);
2348 return <span prop={text}>{this.state.hasMounted}</span>;
2349 }
2350 }
packages/react/src/__tests__/ReactProfilerDOM-test.internal.js
+1 -4
@@ -59,7 +59,6 @@ function loadModules() {
59
60 describe('ProfilerDOM', () => {
61 let TextResource;
62 - let cache;
62 let resourcePromise;
63 let onInteractionScheduledWorkCompleted;
64 let onInteractionTraced;
@@ -81,8 +80,6 @@ describe('ProfilerDOM', () => {
80 onWorkStopped: () => {},
81 });
82
84 - cache = ReactCache.createCache(() => {});
85 -
83 resourcePromise = null;
84
85 TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
@@ -101,7 +98,7 @@ describe('ProfilerDOM', () => {
98 });
99
100 const AsyncText = ({ms, text}) => {
104 - TextResource.read(cache, [text, ms]);
101 + TextResource.read([text, ms]);
102 return text;
103 };
104
scripts/rollup/bundles.js
+1 -1
@@ -377,7 +377,7 @@ const bundles = [
377 moduleType: ISOMORPHIC,
378 entry: 'react-cache',
379 global: 'ReactCache',
380 - externals: ['react'],
380 + externals: ['react', 'scheduler'],
381 },
382
383 /******* createComponentWithSubscriptions (experimental) *******/