[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) *******/