mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[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
This commit is contained in:
Vendored
+142
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* Copyright (c) 2014-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import {unstable_scheduleCallback as scheduleCallback} from 'scheduler';
|
||||
|
||||
type Entry<T> = {|
|
||||
value: T,
|
||||
onDelete: () => mixed,
|
||||
previous: Entry<T>,
|
||||
next: Entry<T>,
|
||||
|};
|
||||
|
||||
export function createLRU<T>(limit: number) {
|
||||
let LIMIT = limit;
|
||||
|
||||
// Circular, doubly-linked list
|
||||
let first: Entry<T> | null = null;
|
||||
let size: number = 0;
|
||||
|
||||
let cleanUpIsScheduled: boolean = false;
|
||||
|
||||
function scheduleCleanUp() {
|
||||
if (cleanUpIsScheduled === false && size > LIMIT) {
|
||||
// The cache size exceeds the limit. Schedule a callback to delete the
|
||||
// least recently used entries.
|
||||
cleanUpIsScheduled = true;
|
||||
scheduleCallback(cleanUp);
|
||||
}
|
||||
}
|
||||
|
||||
function cleanUp() {
|
||||
cleanUpIsScheduled = false;
|
||||
deleteLeastRecentlyUsedEntries(LIMIT);
|
||||
}
|
||||
|
||||
function deleteLeastRecentlyUsedEntries(targetSize: number) {
|
||||
// Delete entries from the cache, starting from the end of the list.
|
||||
if (first !== null) {
|
||||
const resolvedFirst: Entry<T> = (first: any);
|
||||
let last = resolvedFirst.previous;
|
||||
while (size > targetSize && last !== null) {
|
||||
const onDelete = last.onDelete;
|
||||
const previous = last.previous;
|
||||
last.onDelete = (null: any);
|
||||
|
||||
// Remove from the list
|
||||
last.previous = last.next = (null: any);
|
||||
if (last === first) {
|
||||
// Reached the head of the list.
|
||||
first = last = null;
|
||||
} else {
|
||||
(first: any).previous = previous;
|
||||
previous.next = (first: any);
|
||||
last = previous;
|
||||
}
|
||||
|
||||
size -= 1;
|
||||
|
||||
// Call the destroy method after removing the entry from the list. If it
|
||||
// throws, the rest of cache will not be deleted, but it will be in a
|
||||
// valid state.
|
||||
onDelete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function add(value: T, onDelete: () => mixed): Entry<T> {
|
||||
const entry = {
|
||||
value,
|
||||
onDelete,
|
||||
next: (null: any),
|
||||
previous: (null: any),
|
||||
};
|
||||
if (first === null) {
|
||||
entry.previous = entry.next = entry;
|
||||
first = entry;
|
||||
} else {
|
||||
// Append to head
|
||||
const last = first.previous;
|
||||
last.next = entry;
|
||||
entry.previous = last;
|
||||
|
||||
first.previous = entry;
|
||||
entry.next = first;
|
||||
|
||||
first = entry;
|
||||
}
|
||||
size += 1;
|
||||
return entry;
|
||||
}
|
||||
|
||||
function update(entry: Entry<T>, newValue: T): void {
|
||||
entry.value = newValue;
|
||||
}
|
||||
|
||||
function access(entry: Entry<T>): T {
|
||||
const next = entry.next;
|
||||
if (next !== null) {
|
||||
// Entry already cached
|
||||
const resolvedFirst: Entry<T> = (first: any);
|
||||
if (first !== entry) {
|
||||
// Remove from current position
|
||||
const previous = entry.previous;
|
||||
previous.next = next;
|
||||
next.previous = previous;
|
||||
|
||||
// Append to head
|
||||
const last = resolvedFirst.previous;
|
||||
last.next = entry;
|
||||
entry.previous = last;
|
||||
|
||||
resolvedFirst.previous = entry;
|
||||
entry.next = resolvedFirst;
|
||||
|
||||
first = entry;
|
||||
}
|
||||
} else {
|
||||
// Cannot access a deleted entry
|
||||
// TODO: Error? Warning?
|
||||
}
|
||||
scheduleCleanUp();
|
||||
return entry.value;
|
||||
}
|
||||
|
||||
function setLimit(newLimit: number) {
|
||||
LIMIT = newLimit;
|
||||
scheduleCleanUp();
|
||||
}
|
||||
|
||||
return {
|
||||
add,
|
||||
update,
|
||||
access,
|
||||
setLimit,
|
||||
};
|
||||
}
|
||||
+157
-373
@@ -10,394 +10,178 @@
|
||||
import React from 'react';
|
||||
import warningWithoutStack from 'shared/warningWithoutStack';
|
||||
|
||||
function noop() {}
|
||||
import {createLRU} from './LRU';
|
||||
|
||||
const Empty = 0;
|
||||
const Pending = 1;
|
||||
const Resolved = 2;
|
||||
const Rejected = 3;
|
||||
|
||||
type EmptyRecord<K> = {|
|
||||
status: 0,
|
||||
suspender: null,
|
||||
key: K,
|
||||
value: null,
|
||||
error: null,
|
||||
next: any, // TODO: (issue #12941)
|
||||
previous: any, // TODO: (issue #12941)
|
||||
/**
|
||||
* Proper types would be something like this:
|
||||
* next: Record<K, V> | null,
|
||||
* previous: Record<K, V> | null,
|
||||
*/
|
||||
|};
|
||||
|
||||
type PendingRecord<K, V> = {|
|
||||
status: 1,
|
||||
suspender: Promise<V>,
|
||||
key: K,
|
||||
value: null,
|
||||
error: null,
|
||||
next: any, // TODO: (issue #12941)
|
||||
previous: any, // TODO: (issue #12941)
|
||||
/**
|
||||
* Proper types would be something like this:
|
||||
* next: Record<K, V> | null,
|
||||
* previous: Record<K, V> | null,
|
||||
*/
|
||||
|};
|
||||
|
||||
type ResolvedRecord<K, V> = {|
|
||||
status: 2,
|
||||
suspender: null,
|
||||
key: K,
|
||||
value: V,
|
||||
error: null,
|
||||
next: any, // TODO: (issue #12941)
|
||||
previous: any, // TODO: (issue #12941)
|
||||
/**
|
||||
* Proper types would be something like this:
|
||||
* next: Record<K, V> | null,
|
||||
* previous: Record<K, V> | null,
|
||||
*/
|
||||
|};
|
||||
|
||||
type RejectedRecord<K> = {|
|
||||
status: 3,
|
||||
suspender: null,
|
||||
key: K,
|
||||
value: null,
|
||||
error: Error,
|
||||
next: any, // TODO: (issue #12941)
|
||||
previous: any, // TODO: (issue #12941)
|
||||
/**
|
||||
* Proper types would be something like this:
|
||||
* next: Record<K, V> | null,
|
||||
* previous: Record<K, V> | null,
|
||||
*/
|
||||
|};
|
||||
|
||||
type Record<K, V> =
|
||||
| EmptyRecord<K>
|
||||
| PendingRecord<K, V>
|
||||
| ResolvedRecord<K, V>
|
||||
| RejectedRecord<K>;
|
||||
|
||||
type RecordCache<K, V> = {|
|
||||
map: Map<K, Record<K, V>>,
|
||||
head: Record<K, V> | null,
|
||||
tail: Record<K, V> | null,
|
||||
size: number,
|
||||
|};
|
||||
|
||||
// TODO: How do you express this type with Flow?
|
||||
type ResourceMap = Map<any, RecordCache<any, any>>;
|
||||
type Cache = {
|
||||
invalidate(): void,
|
||||
read<K, V, A>(
|
||||
resourceType: mixed,
|
||||
key: K,
|
||||
miss: (A) => Promise<V>,
|
||||
missArg: A,
|
||||
): V,
|
||||
preload<K, V, A>(
|
||||
resourceType: mixed,
|
||||
key: K,
|
||||
miss: (A) => Promise<V>,
|
||||
missArg: A,
|
||||
): void,
|
||||
|
||||
// DEV-only
|
||||
$$typeof?: Symbol | number,
|
||||
type Thenable<T> = {
|
||||
then(resolve: (T) => mixed, reject: (mixed) => mixed): mixed,
|
||||
};
|
||||
|
||||
let CACHE_TYPE;
|
||||
if (__DEV__) {
|
||||
CACHE_TYPE = 0xcac4e;
|
||||
}
|
||||
type Suspender = {
|
||||
then(resolve: () => mixed, reject: () => mixed): mixed,
|
||||
};
|
||||
|
||||
let isCache;
|
||||
if (__DEV__) {
|
||||
isCache = value =>
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
value.$$typeof === CACHE_TYPE;
|
||||
}
|
||||
|
||||
// TODO: Make this configurable per resource
|
||||
const MAX_SIZE = 500;
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
function createRecord<K>(key: K): EmptyRecord<K> {
|
||||
return {
|
||||
status: Empty,
|
||||
suspender: null,
|
||||
key,
|
||||
value: null,
|
||||
error: null,
|
||||
next: null,
|
||||
previous: null,
|
||||
};
|
||||
}
|
||||
|
||||
function createRecordCache<K, V>(): RecordCache<K, V> {
|
||||
return {
|
||||
map: new Map(),
|
||||
head: null,
|
||||
tail: null,
|
||||
size: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function createCache(invalidator: () => mixed): Cache {
|
||||
const resourceMap: ResourceMap = new Map();
|
||||
|
||||
function accessRecord<K, V>(resourceType: any, key: K): Record<K, V> {
|
||||
if (__DEV__) {
|
||||
warningWithoutStack(
|
||||
typeof resourceType !== 'string' && typeof resourceType !== 'number',
|
||||
'Invalid resourceType: Expected a symbol, object, or function, but ' +
|
||||
'instead received: %s. Strings and numbers are not permitted as ' +
|
||||
'resource types.',
|
||||
resourceType,
|
||||
);
|
||||
}
|
||||
|
||||
let recordCache = resourceMap.get(resourceType);
|
||||
if (recordCache === undefined) {
|
||||
recordCache = createRecordCache();
|
||||
resourceMap.set(resourceType, recordCache);
|
||||
}
|
||||
const map = recordCache.map;
|
||||
|
||||
let record = map.get(key);
|
||||
if (record === undefined) {
|
||||
// This record does not already exist. Create a new one.
|
||||
record = createRecord(key);
|
||||
map.set(key, record);
|
||||
if (recordCache.size >= MAX_SIZE) {
|
||||
// The cache is already at maximum capacity. Remove PAGE_SIZE least
|
||||
// recently used records.
|
||||
// TODO: We assume the max capcity is greater than zero. Otherwise warn.
|
||||
const tail = recordCache.tail;
|
||||
if (tail !== null) {
|
||||
let newTail = tail;
|
||||
for (let i = 0; i < PAGE_SIZE && newTail !== null; i++) {
|
||||
recordCache.size -= 1;
|
||||
map.delete(newTail.key);
|
||||
newTail = newTail.previous;
|
||||
}
|
||||
recordCache.tail = newTail;
|
||||
if (newTail !== null) {
|
||||
newTail.next = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This record is already cached. Remove it from its current position in
|
||||
// the list. We'll add it to the front below.
|
||||
const previous = record.previous;
|
||||
const next = record.next;
|
||||
if (previous !== null) {
|
||||
previous.next = next;
|
||||
} else {
|
||||
recordCache.head = next;
|
||||
}
|
||||
if (next !== null) {
|
||||
next.previous = previous;
|
||||
} else {
|
||||
recordCache.tail = previous;
|
||||
}
|
||||
recordCache.size -= 1;
|
||||
}
|
||||
|
||||
// Add the record to the front of the list.
|
||||
const head = recordCache.head;
|
||||
const newHead = record;
|
||||
recordCache.head = newHead;
|
||||
newHead.previous = null;
|
||||
newHead.next = head;
|
||||
if (head !== null) {
|
||||
head.previous = newHead;
|
||||
} else {
|
||||
recordCache.tail = newHead;
|
||||
}
|
||||
recordCache.size += 1;
|
||||
|
||||
return newHead;
|
||||
}
|
||||
|
||||
function load<K, V>(emptyRecord: EmptyRecord<K>, suspender: Promise<V>) {
|
||||
const pendingRecord: PendingRecord<K, V> = (emptyRecord: any);
|
||||
pendingRecord.status = Pending;
|
||||
pendingRecord.suspender = suspender;
|
||||
suspender.then(
|
||||
value => {
|
||||
// Resource loaded successfully.
|
||||
const resolvedRecord: ResolvedRecord<K, V> = (pendingRecord: any);
|
||||
resolvedRecord.status = Resolved;
|
||||
resolvedRecord.suspender = null;
|
||||
resolvedRecord.value = value;
|
||||
},
|
||||
error => {
|
||||
// Resource failed to load. Stash the error for later so we can throw it
|
||||
// the next time it's requested.
|
||||
const rejectedRecord: RejectedRecord<K> = (pendingRecord: any);
|
||||
rejectedRecord.status = Rejected;
|
||||
rejectedRecord.suspender = null;
|
||||
rejectedRecord.error = error;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const cache: Cache = {
|
||||
invalidate() {
|
||||
invalidator();
|
||||
},
|
||||
preload<K, V, A>(
|
||||
resourceType: any,
|
||||
key: K,
|
||||
miss: A => Promise<V>,
|
||||
missArg: A,
|
||||
): void {
|
||||
const record: Record<K, V> = accessRecord(resourceType, key);
|
||||
switch (record.status) {
|
||||
case Empty:
|
||||
// Warm the cache.
|
||||
const suspender = miss(missArg);
|
||||
load(record, suspender);
|
||||
return;
|
||||
case Pending:
|
||||
// There's already a pending request.
|
||||
return;
|
||||
case Resolved:
|
||||
// The resource is already in the cache.
|
||||
return;
|
||||
case Rejected:
|
||||
// The request failed.
|
||||
return;
|
||||
}
|
||||
},
|
||||
read<K, V, A>(
|
||||
resourceType: any,
|
||||
key: K,
|
||||
miss: A => Promise<V>,
|
||||
missArg: A,
|
||||
): V {
|
||||
const record: Record<K, V> = accessRecord(resourceType, key);
|
||||
switch (record.status) {
|
||||
case Empty:
|
||||
// Load the requested resource.
|
||||
const suspender = miss(missArg);
|
||||
load(record, suspender);
|
||||
throw suspender;
|
||||
case Pending:
|
||||
// There's already a pending request.
|
||||
throw record.suspender;
|
||||
case Resolved:
|
||||
return record.value;
|
||||
case Rejected:
|
||||
default:
|
||||
// The requested resource previously failed loading.
|
||||
const error = record.error;
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (__DEV__) {
|
||||
cache.$$typeof = CACHE_TYPE;
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
let warnIfNonPrimitiveKey;
|
||||
if (__DEV__) {
|
||||
warnIfNonPrimitiveKey = (key, methodName) => {
|
||||
warningWithoutStack(
|
||||
typeof key === 'string' ||
|
||||
typeof key === 'number' ||
|
||||
typeof key === 'boolean' ||
|
||||
key === undefined ||
|
||||
key === null,
|
||||
'%s: Invalid key type. Expected a string, number, symbol, or boolean, ' +
|
||||
'but instead received: %s' +
|
||||
'\n\nTo use non-primitive values as keys, you must pass a hash ' +
|
||||
'function as the second argument to unstable_createResource().',
|
||||
methodName,
|
||||
key,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
type primitive = string | number | boolean | void | null;
|
||||
|
||||
type Resource<K, V> = {|
|
||||
read(Cache, K): V,
|
||||
preload(cache: Cache, key: K): void,
|
||||
type PendingResult = {|
|
||||
status: 0,
|
||||
value: Suspender,
|
||||
|};
|
||||
|
||||
// These declarations are used to express function overloading. I wish there
|
||||
// were a more elegant way to do this in the function definition itself.
|
||||
type ResolvedResult<V> = {|
|
||||
status: 1,
|
||||
value: V,
|
||||
|};
|
||||
|
||||
// Primitive keys do not request a hash function.
|
||||
declare function unstable_createResource<V, K: primitive, H: primitive>(
|
||||
loadResource: (K) => Promise<V>,
|
||||
hash?: (K) => H,
|
||||
): Resource<K, V>;
|
||||
type RejectedResult = {|
|
||||
status: 2,
|
||||
value: mixed,
|
||||
|};
|
||||
|
||||
// Non-primitive keys *do* require a hash function.
|
||||
// eslint-disable-next-line no-redeclare
|
||||
declare function unstable_createResource<V, K: mixed, H: primitive>(
|
||||
loadResource: (K) => Promise<V>,
|
||||
hash: (K) => H,
|
||||
): Resource<K, V>;
|
||||
type Result<V> = PendingResult | ResolvedResult<V> | RejectedResult;
|
||||
|
||||
type Resource<I, V> = {
|
||||
read(I): V,
|
||||
preload(I): void,
|
||||
};
|
||||
|
||||
const Pending = 0;
|
||||
const Resolved = 1;
|
||||
const Rejected = 2;
|
||||
|
||||
const currentOwner =
|
||||
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner;
|
||||
|
||||
function readContext(Context, observedBits) {
|
||||
const dispatcher = currentOwner.currentDispatcher;
|
||||
if (dispatcher === null) {
|
||||
throw new Error(
|
||||
'react-cache: read and preload may only be called from within a ' +
|
||||
"component's render. They are not supported in event handlers or " +
|
||||
'lifecycle methods.',
|
||||
);
|
||||
}
|
||||
return dispatcher.readContext(Context, observedBits);
|
||||
}
|
||||
|
||||
function identityHashFn(input) {
|
||||
if (__DEV__) {
|
||||
warningWithoutStack(
|
||||
typeof input === 'string' ||
|
||||
typeof input === 'number' ||
|
||||
typeof input === 'boolean' ||
|
||||
input === undefined ||
|
||||
input === null,
|
||||
'Invalid key type. Expected a string, number, symbol, or boolean, ' +
|
||||
'but instead received: %s' +
|
||||
'\n\nTo use non-primitive values as keys, you must pass a hash ' +
|
||||
'function as the second argument to createResource().',
|
||||
input,
|
||||
);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
const CACHE_LIMIT = 500;
|
||||
const lru = createLRU(CACHE_LIMIT);
|
||||
|
||||
const entries: Map<Resource<any, any>, Map<any, any>> = new Map();
|
||||
|
||||
const CacheContext = React.createContext(null);
|
||||
|
||||
function accessResult<I, K, V>(
|
||||
resource: any,
|
||||
fetch: I => Thenable<V>,
|
||||
input: I,
|
||||
key: K,
|
||||
): Result<V> {
|
||||
let entriesForResource = entries.get(resource);
|
||||
if (entriesForResource === undefined) {
|
||||
entriesForResource = new Map();
|
||||
entries.set(resource, entriesForResource);
|
||||
}
|
||||
let entry = entriesForResource.get(key);
|
||||
if (entry === undefined) {
|
||||
const thenable = fetch(input);
|
||||
thenable.then(
|
||||
value => {
|
||||
if (newResult.status === Pending) {
|
||||
const resolvedResult: ResolvedResult<V> = (newResult: any);
|
||||
resolvedResult.status = Resolved;
|
||||
resolvedResult.value = value;
|
||||
}
|
||||
},
|
||||
error => {
|
||||
if (newResult.status === Pending) {
|
||||
const rejectedResult: RejectedResult = (newResult: any);
|
||||
rejectedResult.status = Rejected;
|
||||
rejectedResult.value = error;
|
||||
}
|
||||
},
|
||||
);
|
||||
const newResult: PendingResult = {
|
||||
status: Pending,
|
||||
value: thenable,
|
||||
};
|
||||
const newEntry = lru.add(newResult, deleteEntry.bind(null, resource, key));
|
||||
entriesForResource.set(key, newEntry);
|
||||
return newResult;
|
||||
} else {
|
||||
return (lru.access(entry): any);
|
||||
}
|
||||
}
|
||||
|
||||
function deleteEntry(resource, key) {
|
||||
const entriesForResource = entries.get(resource);
|
||||
if (entriesForResource !== undefined) {
|
||||
entriesForResource.delete(key);
|
||||
if (entriesForResource.size === 0) {
|
||||
entries.delete(resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function unstable_createResource<I, K: string | number, V>(
|
||||
fetch: I => Thenable<V>,
|
||||
maybeHashInput?: I => K,
|
||||
): Resource<I, V> {
|
||||
const hashInput: I => K =
|
||||
maybeHashInput !== undefined ? maybeHashInput : (identityHashFn: any);
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export function unstable_createResource<V, K, H: primitive>(
|
||||
loadResource: K => Promise<V>,
|
||||
hash: K => H,
|
||||
): Resource<K, V> {
|
||||
const resource = {
|
||||
read(cache, key) {
|
||||
if (__DEV__) {
|
||||
warningWithoutStack(
|
||||
isCache(cache),
|
||||
'read(): The first argument must be a cache. Instead received: %s',
|
||||
cache,
|
||||
);
|
||||
}
|
||||
if (hash === undefined) {
|
||||
if (__DEV__) {
|
||||
warnIfNonPrimitiveKey(key, 'read');
|
||||
read(input: I): V {
|
||||
// react-cache currently doesn't rely on context, but it may in the
|
||||
// future, so we read anyway to prevent access outside of render.
|
||||
readContext(CacheContext);
|
||||
const key = hashInput(input);
|
||||
const result: Result<V> = accessResult(resource, fetch, input, key);
|
||||
switch (result.status) {
|
||||
case Pending: {
|
||||
const suspender = result.value;
|
||||
throw suspender;
|
||||
}
|
||||
return cache.read(resource, key, loadResource, key);
|
||||
case Resolved: {
|
||||
const value = result.value;
|
||||
return value;
|
||||
}
|
||||
case Rejected: {
|
||||
const error = result.value;
|
||||
throw error;
|
||||
}
|
||||
default:
|
||||
// Should be unreachable
|
||||
return (undefined: any);
|
||||
}
|
||||
const hashedKey = hash(key);
|
||||
return cache.read(resource, hashedKey, loadResource, key);
|
||||
},
|
||||
preload(cache, key) {
|
||||
if (__DEV__) {
|
||||
warningWithoutStack(
|
||||
isCache(cache),
|
||||
'preload(): The first argument must be a cache. Instead received: %s',
|
||||
cache,
|
||||
);
|
||||
}
|
||||
if (hash === undefined) {
|
||||
if (__DEV__) {
|
||||
warnIfNonPrimitiveKey(key, 'preload');
|
||||
}
|
||||
cache.preload(resource, key, loadResource, key);
|
||||
return;
|
||||
}
|
||||
const hashedKey = hash(key);
|
||||
cache.preload(resource, hashedKey, loadResource, key);
|
||||
|
||||
preload(input: I): void {
|
||||
// react-cache currently doesn't rely on context, but it may in the
|
||||
// future, so we read anyway to prevent access outside of render.
|
||||
readContext(CacheContext);
|
||||
const key = hashInput(input);
|
||||
accessResult(resource, fetch, input, key);
|
||||
},
|
||||
};
|
||||
return resource;
|
||||
}
|
||||
|
||||
// Global cache has no eviction policy (except for, ya know, a browser refresh).
|
||||
const globalCache = createCache(noop);
|
||||
export const ReactCache = React.createContext(globalCache);
|
||||
export function unstable_setGlobalCacheLimit(limit: number) {
|
||||
lru.setLimit(limit);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
/**
|
||||
* Copyright (c) 2013-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let ReactCache;
|
||||
let createResource;
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactTestRenderer;
|
||||
let Suspense;
|
||||
let TextResource;
|
||||
let textResourceShouldFail;
|
||||
let flushScheduledWork;
|
||||
let evictLRU;
|
||||
|
||||
describe('ReactCache', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
||||
jest.mock('scheduler', () => {
|
||||
let callbacks = [];
|
||||
return {
|
||||
unstable_scheduleCallback(callback) {
|
||||
const callbackIndex = callbacks.length;
|
||||
callbacks.push(callback);
|
||||
return {callbackIndex};
|
||||
},
|
||||
flushScheduledWork() {
|
||||
while (callbacks.length) {
|
||||
const callback = callbacks.pop();
|
||||
callback();
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
|
||||
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
|
||||
React = require('react');
|
||||
Suspense = React.Suspense;
|
||||
ReactCache = require('react-cache');
|
||||
createResource = ReactCache.unstable_createResource;
|
||||
ReactTestRenderer = require('react-test-renderer');
|
||||
flushScheduledWork = require('scheduler').flushScheduledWork;
|
||||
evictLRU = flushScheduledWork;
|
||||
|
||||
TextResource = createResource(([text, ms = 0]) => {
|
||||
let listeners = null;
|
||||
let status = 'pending';
|
||||
let value = null;
|
||||
return {
|
||||
then(resolve, reject) {
|
||||
switch (status) {
|
||||
case 'pending': {
|
||||
if (listeners === null) {
|
||||
listeners = [{resolve, reject}];
|
||||
setTimeout(() => {
|
||||
if (textResourceShouldFail) {
|
||||
ReactTestRenderer.unstable_yield(
|
||||
`Promise rejected [${text}]`,
|
||||
);
|
||||
status = 'rejected';
|
||||
value = new Error('Failed to load: ' + text);
|
||||
listeners.forEach(listener => listener.reject(value));
|
||||
} else {
|
||||
ReactTestRenderer.unstable_yield(
|
||||
`Promise resolved [${text}]`,
|
||||
);
|
||||
status = 'resolved';
|
||||
value = text;
|
||||
listeners.forEach(listener => listener.resolve(value));
|
||||
}
|
||||
}, ms);
|
||||
} else {
|
||||
listeners.push({resolve, reject});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'resolved': {
|
||||
resolve(value);
|
||||
break;
|
||||
}
|
||||
case 'rejected': {
|
||||
reject(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}, ([text, ms]) => text);
|
||||
|
||||
textResourceShouldFail = false;
|
||||
});
|
||||
|
||||
function Text(props) {
|
||||
ReactTestRenderer.unstable_yield(props.text);
|
||||
return props.text;
|
||||
}
|
||||
|
||||
function AsyncText(props) {
|
||||
const text = props.text;
|
||||
try {
|
||||
TextResource.read([props.text, props.ms]);
|
||||
ReactTestRenderer.unstable_yield(text);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
ReactTestRenderer.unstable_yield(`Suspend! [${text}]`);
|
||||
} else {
|
||||
ReactTestRenderer.unstable_yield(`Error! [${text}]`);
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
}
|
||||
|
||||
it('throws a promise if the requested value is not in the cache', () => {
|
||||
function App() {
|
||||
return (
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText ms={100} text="Hi" />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(<App />, {
|
||||
unstable_isConcurrent: true,
|
||||
});
|
||||
|
||||
expect(root).toFlushAndYield(['Suspend! [Hi]', 'Loading...']);
|
||||
|
||||
jest.advanceTimersByTime(100);
|
||||
expect(ReactTestRenderer).toHaveYielded(['Promise resolved [Hi]']);
|
||||
expect(root).toFlushAndYield(['Hi']);
|
||||
});
|
||||
|
||||
it('throws an error on the subsequent read if the promise is rejected', async () => {
|
||||
function App() {
|
||||
return (
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText ms={100} text="Hi" />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(<App />, {
|
||||
unstable_isConcurrent: true,
|
||||
});
|
||||
|
||||
expect(root).toFlushAndYield(['Suspend! [Hi]', 'Loading...']);
|
||||
|
||||
textResourceShouldFail = true;
|
||||
jest.advanceTimersByTime(100);
|
||||
expect(ReactTestRenderer).toHaveYielded(['Promise rejected [Hi]']);
|
||||
|
||||
expect(root).toFlushAndThrow('Failed to load: Hi');
|
||||
expect(ReactTestRenderer).toHaveYielded(['Error! [Hi]', 'Error! [Hi]']);
|
||||
|
||||
// Should throw again on a subsequent read
|
||||
root.update(<App />);
|
||||
expect(root).toFlushAndThrow('Failed to load: Hi');
|
||||
expect(ReactTestRenderer).toHaveYielded(['Error! [Hi]', 'Error! [Hi]']);
|
||||
});
|
||||
|
||||
it('warns if non-primitive key is passed to a resource without a hash function', () => {
|
||||
const BadTextResource = createResource(([text, ms = 0]) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
resolve(text);
|
||||
}, ms),
|
||||
);
|
||||
});
|
||||
|
||||
function App() {
|
||||
ReactTestRenderer.unstable_yield('App');
|
||||
return BadTextResource.read(['Hi', 100]);
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<App />
|
||||
</Suspense>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (__DEV__) {
|
||||
expect(() => {
|
||||
expect(root).toFlushAndYield(['App', 'Loading...']);
|
||||
}).toWarnDev(
|
||||
[
|
||||
'Invalid key type. Expected a string, number, symbol, or ' +
|
||||
'boolean, but instead received: Hi,100\n\n' +
|
||||
'To use non-primitive values as keys, you must pass a hash ' +
|
||||
'function as the second argument to createResource().',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
} else {
|
||||
expect(root).toFlushAndYield(['App', 'Loading...']);
|
||||
}
|
||||
});
|
||||
|
||||
it('evicts least recently used values', async () => {
|
||||
ReactCache.unstable_setGlobalCacheLimit(3);
|
||||
|
||||
// Render 1, 2, and 3
|
||||
const root = ReactTestRenderer.create(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText ms={100} text={1} />
|
||||
<AsyncText ms={100} text={2} />
|
||||
<AsyncText ms={100} text={3} />
|
||||
</Suspense>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
expect(root).toFlushAndYield([
|
||||
'Suspend! [1]',
|
||||
'Suspend! [2]',
|
||||
'Suspend! [3]',
|
||||
'Loading...',
|
||||
]);
|
||||
jest.advanceTimersByTime(100);
|
||||
expect(ReactTestRenderer).toHaveYielded([
|
||||
'Promise resolved [1]',
|
||||
'Promise resolved [2]',
|
||||
'Promise resolved [3]',
|
||||
]);
|
||||
expect(root).toFlushAndYield([1, 2, 3]);
|
||||
expect(root).toMatchRenderedOutput('123');
|
||||
|
||||
// Render 1, 4, 5
|
||||
root.update(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText ms={100} text={1} />
|
||||
<AsyncText ms={100} text={4} />
|
||||
<AsyncText ms={100} text={5} />
|
||||
</Suspense>,
|
||||
);
|
||||
|
||||
expect(root).toFlushAndYield([
|
||||
1,
|
||||
'Suspend! [4]',
|
||||
'Suspend! [5]',
|
||||
'Loading...',
|
||||
]);
|
||||
jest.advanceTimersByTime(100);
|
||||
expect(ReactTestRenderer).toHaveYielded([
|
||||
'Promise resolved [4]',
|
||||
'Promise resolved [5]',
|
||||
]);
|
||||
expect(root).toFlushAndYield([1, 4, 5]);
|
||||
expect(root).toMatchRenderedOutput('145');
|
||||
|
||||
// We've now rendered values 1, 2, 3, 4, 5, over our limit of 3. The least
|
||||
// recently used values are 2 and 3. They will be evicted during the
|
||||
// next sweep.
|
||||
evictLRU();
|
||||
|
||||
root.update(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<AsyncText ms={100} text={1} />
|
||||
<AsyncText ms={100} text={2} />
|
||||
<AsyncText ms={100} text={3} />
|
||||
</Suspense>,
|
||||
);
|
||||
|
||||
expect(root).toFlushAndYield([
|
||||
// 1 is still cached
|
||||
1,
|
||||
// 2 and 3 suspend because they were evicted from the cache
|
||||
'Suspend! [2]',
|
||||
'Suspend! [3]',
|
||||
'Loading...',
|
||||
]);
|
||||
jest.advanceTimersByTime(100);
|
||||
expect(ReactTestRenderer).toHaveYielded([
|
||||
'Promise resolved [2]',
|
||||
'Promise resolved [3]',
|
||||
]);
|
||||
expect(root).toFlushAndYield([1, 2, 3]);
|
||||
expect(root).toMatchRenderedOutput('123');
|
||||
});
|
||||
|
||||
it('preloads during the render phase', async () => {
|
||||
function App() {
|
||||
TextResource.preload(['B', 1000]);
|
||||
TextResource.read(['A', 1000]);
|
||||
TextResource.read(['B', 1000]);
|
||||
return <Text text="Result" />;
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<App />
|
||||
</Suspense>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(root).toFlushAndYield(['Loading...']);
|
||||
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(ReactTestRenderer).toHaveYielded([
|
||||
'Promise resolved [B]',
|
||||
'Promise resolved [A]',
|
||||
]);
|
||||
expect(root).toFlushAndYield(['Result']);
|
||||
expect(root).toMatchRenderedOutput('Result');
|
||||
});
|
||||
|
||||
it('if a thenable resolves multiple times, does not update the first cached value', () => {
|
||||
let resolveThenable;
|
||||
const BadTextResource = createResource(([text, ms = 0]) => {
|
||||
let listeners = null;
|
||||
let value = null;
|
||||
return {
|
||||
then(resolve, reject) {
|
||||
if (value !== null) {
|
||||
resolve(value);
|
||||
} else {
|
||||
if (listeners === null) {
|
||||
listeners = [resolve];
|
||||
resolveThenable = v => {
|
||||
listeners.forEach(listener => listener(v));
|
||||
};
|
||||
} else {
|
||||
listeners.push(resolve);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}, ([text, ms]) => text);
|
||||
|
||||
function BadAsyncText(props) {
|
||||
const text = props.text;
|
||||
try {
|
||||
const actualText = BadTextResource.read([props.text, props.ms]);
|
||||
ReactTestRenderer.unstable_yield(actualText);
|
||||
return actualText;
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
ReactTestRenderer.unstable_yield(`Suspend! [${text}]`);
|
||||
} else {
|
||||
ReactTestRenderer.unstable_yield(`Error! [${text}]`);
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<BadAsyncText text="Hi" />
|
||||
</Suspense>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(root).toFlushAndYield(['Suspend! [Hi]', 'Loading...']);
|
||||
|
||||
resolveThenable('Hi');
|
||||
// This thenable improperly resolves twice. We should not update the
|
||||
// cached value.
|
||||
resolveThenable('Hi muahahaha I am different');
|
||||
|
||||
root.update(
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<BadAsyncText text="Hi" />
|
||||
</Suspense>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(ReactTestRenderer).toHaveYielded([]);
|
||||
expect(root).toFlushAndYield(['Hi']);
|
||||
expect(root).toMatchRenderedOutput('Hi');
|
||||
});
|
||||
|
||||
it('throws if read is called outside render', () => {
|
||||
expect(() => TextResource.read(['A', 1000])).toThrow(
|
||||
"read and preload may only be called from within a component's render",
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if preload is called outside render', () => {
|
||||
expect(() => TextResource.preload(['A', 1000])).toThrow(
|
||||
"read and preload may only be called from within a component's render",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,235 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let ReactCache;
|
||||
|
||||
describe('ReactCache', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
ReactCache = require('react-cache');
|
||||
});
|
||||
|
||||
it('throws a promise if the requested value is not in the cache', async () => {
|
||||
const {createCache, unstable_createResource} = ReactCache;
|
||||
|
||||
function loadUpperCase(text) {
|
||||
return Promise.resolve(text.toUpperCase());
|
||||
}
|
||||
const UpperCase = unstable_createResource(loadUpperCase);
|
||||
const cache = createCache();
|
||||
|
||||
let suspender;
|
||||
try {
|
||||
UpperCase.read(cache, 'hello');
|
||||
} catch (v) {
|
||||
suspender = v;
|
||||
}
|
||||
|
||||
await suspender;
|
||||
const result = UpperCase.read(cache, 'hello');
|
||||
expect(result).toBe('HELLO');
|
||||
});
|
||||
|
||||
it('throws an error on the subsequent read if the promise is rejected', async () => {
|
||||
const {createCache, unstable_createResource} = ReactCache;
|
||||
|
||||
let shouldFail = true;
|
||||
function loadUpperCase(text) {
|
||||
if (shouldFail) {
|
||||
// Rejects on the first try
|
||||
shouldFail = false;
|
||||
return Promise.reject(new Error('oh no'));
|
||||
} else {
|
||||
// Succeeds the second time
|
||||
return Promise.resolve(text.toUpperCase());
|
||||
}
|
||||
}
|
||||
const UpperCase = unstable_createResource(loadUpperCase);
|
||||
const cache = createCache();
|
||||
|
||||
let suspender;
|
||||
try {
|
||||
UpperCase.read(cache, 'hello');
|
||||
} catch (v) {
|
||||
suspender = v;
|
||||
}
|
||||
|
||||
let error;
|
||||
try {
|
||||
await suspender;
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(() => UpperCase.read(cache, 'hello')).toThrow(error);
|
||||
expect(error.message).toBe('oh no');
|
||||
|
||||
// On a subsequent read, it should still throw.
|
||||
try {
|
||||
UpperCase.read(cache, 'hello');
|
||||
} catch (v) {
|
||||
suspender = v;
|
||||
}
|
||||
await suspender;
|
||||
expect(() => UpperCase.read(cache, 'hello')).toThrow(error);
|
||||
expect(error.message).toBe('oh no');
|
||||
});
|
||||
|
||||
it('can preload data ahead of time', async () => {
|
||||
const {createCache, unstable_createResource} = ReactCache;
|
||||
|
||||
function loadUpperCase(text) {
|
||||
return Promise.resolve(text.toUpperCase());
|
||||
}
|
||||
const UpperCase = unstable_createResource(loadUpperCase);
|
||||
const cache = createCache();
|
||||
|
||||
UpperCase.preload(cache, 'hello');
|
||||
// Wait for next tick
|
||||
await Promise.resolve();
|
||||
const result = UpperCase.read(cache, 'hello');
|
||||
expect(result).toBe('HELLO');
|
||||
});
|
||||
|
||||
it('does not throw if preloaded promise rejects', async () => {
|
||||
const {createCache, unstable_createResource} = ReactCache;
|
||||
|
||||
function loadUpperCase(text) {
|
||||
return Promise.reject(new Error('uh oh'));
|
||||
}
|
||||
const UpperCase = unstable_createResource(loadUpperCase);
|
||||
const cache = createCache();
|
||||
|
||||
UpperCase.preload(cache, 'hello');
|
||||
// Wait for next tick
|
||||
await Promise.resolve();
|
||||
|
||||
expect(() => UpperCase.read(cache, 'hello')).toThrow('uh oh');
|
||||
});
|
||||
|
||||
it('accepts custom hash function', async () => {
|
||||
const {createCache, unstable_createResource} = ReactCache;
|
||||
|
||||
function loadSum([a, b]) {
|
||||
return Promise.resolve(a + b);
|
||||
}
|
||||
function hash([a, b]) {
|
||||
return `${a + b}`;
|
||||
}
|
||||
const Sum = unstable_createResource(loadSum, hash);
|
||||
const cache = createCache();
|
||||
|
||||
Sum.preload(cache, [5, 5]);
|
||||
Sum.preload(cache, [1, 2]);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(Sum.read(cache, [5, 5])).toEqual(10);
|
||||
expect(Sum.read(cache, [1, 2])).toEqual(3);
|
||||
// The fact that the next line returns synchronously and doesn't throw, even
|
||||
// though [3, 7] was not preloaded, proves that the hashing function works.
|
||||
expect(Sum.read(cache, [3, 7])).toEqual(10);
|
||||
});
|
||||
|
||||
it('warns if resourceType is a string or number', () => {
|
||||
const {createCache} = ReactCache;
|
||||
|
||||
spyOnDev(console, 'error');
|
||||
const cache = createCache();
|
||||
|
||||
function fn() {
|
||||
cache.preload('foo', 'uppercaseA', () => Promise.resolve('A'));
|
||||
cache.preload(123, 'productOf9And2', () => Promise.resolve(18));
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
expect(fn).toWarnDev(
|
||||
[
|
||||
'Invalid resourceType: Expected a symbol, object, or function, but ' +
|
||||
'instead received: foo. Strings and numbers are not permitted as ' +
|
||||
'resource types.',
|
||||
'Invalid resourceType: Expected a symbol, object, or function, but ' +
|
||||
'instead received: 123. Strings and numbers are not permitted as ' +
|
||||
'resource types.',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
});
|
||||
|
||||
it('warns if non-primitive key is passed to a resource without a hash function', () => {
|
||||
const {createCache, unstable_createResource} = ReactCache;
|
||||
|
||||
spyOnDev(console, 'error');
|
||||
|
||||
function loadSum([a, b]) {
|
||||
return Promise.resolve(a + b);
|
||||
}
|
||||
|
||||
const Sum = unstable_createResource(loadSum);
|
||||
const cache = createCache();
|
||||
|
||||
function fn() {
|
||||
Sum.preload(cache, [5, 5]);
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
expect(fn).toWarnDev(
|
||||
[
|
||||
'preload: Invalid key type. Expected a string, number, symbol, or ' +
|
||||
'boolean, but instead received: 5,5\n\n' +
|
||||
'To use non-primitive values as keys, you must pass a hash ' +
|
||||
'function as the second argument to unstable_createResource().',
|
||||
],
|
||||
{withoutStack: true},
|
||||
);
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
});
|
||||
|
||||
it('stays within maximum capacity by evicting the least recently used record', async () => {
|
||||
const {createCache, unstable_createResource} = ReactCache;
|
||||
|
||||
function loadIntegerString(int) {
|
||||
return Promise.resolve(int + '');
|
||||
}
|
||||
const IntegerStringResource = unstable_createResource(loadIntegerString);
|
||||
const cache = createCache();
|
||||
|
||||
// TODO: This is hard-coded to a maximum size of 500. Make this configurable
|
||||
// per resource.
|
||||
for (let n = 1; n <= 500; n++) {
|
||||
IntegerStringResource.preload(cache, n);
|
||||
}
|
||||
|
||||
// Access 1, 2, and 3 again. The least recently used integer is now 4.
|
||||
IntegerStringResource.preload(cache, 3);
|
||||
IntegerStringResource.preload(cache, 2);
|
||||
IntegerStringResource.preload(cache, 1);
|
||||
|
||||
// Evict older integers from the cache by adding new ones.
|
||||
IntegerStringResource.preload(cache, 501);
|
||||
IntegerStringResource.preload(cache, 502);
|
||||
IntegerStringResource.preload(cache, 503);
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
// 1, 2, and 3 should be in the cache. 4, 5, and 6 should have been evicted.
|
||||
expect(IntegerStringResource.read(cache, 1)).toEqual('1');
|
||||
expect(IntegerStringResource.read(cache, 2)).toEqual('2');
|
||||
expect(IntegerStringResource.read(cache, 3)).toEqual('3');
|
||||
|
||||
expect(() => IntegerStringResource.read(cache, 4)).toThrow(Promise);
|
||||
expect(() => IntegerStringResource.read(cache, 5)).toThrow(Promise);
|
||||
expect(() => IntegerStringResource.read(cache, 6)).toThrow(Promise);
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,6 @@ let React;
|
||||
let ReactDOM;
|
||||
let Suspense;
|
||||
let ReactCache;
|
||||
let cache;
|
||||
let TextResource;
|
||||
|
||||
describe('ReactDOMSuspensePlaceholder', () => {
|
||||
@@ -27,10 +26,6 @@ describe('ReactDOMSuspensePlaceholder', () => {
|
||||
Suspense = React.Suspense;
|
||||
container = document.createElement('div');
|
||||
|
||||
function invalidateCache() {
|
||||
cache = ReactCache.createCache(invalidateCache);
|
||||
}
|
||||
invalidateCache();
|
||||
TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
@@ -59,7 +54,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
|
||||
|
||||
function AsyncText(props) {
|
||||
const text = props.text;
|
||||
TextResource.read(cache, [props.text, props.ms]);
|
||||
TextResource.read([props.text, props.ms]);
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ let Suspense;
|
||||
|
||||
// let JestReact;
|
||||
|
||||
let cache;
|
||||
let TextResource;
|
||||
let textResourceShouldFail;
|
||||
|
||||
@@ -25,10 +24,6 @@ describe('ReactSuspense', () => {
|
||||
|
||||
Suspense = React.Suspense;
|
||||
|
||||
function invalidateCache() {
|
||||
cache = ReactCache.createCache(invalidateCache);
|
||||
}
|
||||
invalidateCache();
|
||||
TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
|
||||
let listeners = null;
|
||||
let status = 'pending';
|
||||
@@ -84,7 +79,7 @@ describe('ReactSuspense', () => {
|
||||
function AsyncText(props) {
|
||||
const text = props.text;
|
||||
try {
|
||||
TextResource.read(cache, [props.text, props.ms]);
|
||||
TextResource.read([props.text, props.ms]);
|
||||
ReactTestRenderer.unstable_yield(text);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
@@ -362,7 +357,7 @@ describe('ReactSuspense', () => {
|
||||
const text = `${this.props.text}:${this.state.step}`;
|
||||
const ms = this.props.ms;
|
||||
try {
|
||||
TextResource.read(cache, [text, ms]);
|
||||
TextResource.read([text, ms]);
|
||||
ReactTestRenderer.unstable_yield(text);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
@@ -501,7 +496,7 @@ describe('ReactSuspense', () => {
|
||||
const text = this.props.text;
|
||||
const ms = this.props.ms;
|
||||
try {
|
||||
TextResource.read(cache, [text, ms]);
|
||||
TextResource.read([text, ms]);
|
||||
ReactTestRenderer.unstable_yield(text);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
|
||||
@@ -21,8 +21,6 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
|
||||
let ReactFeatureFlags;
|
||||
let ReactCache;
|
||||
let Suspense;
|
||||
|
||||
let cache;
|
||||
let TextResource;
|
||||
let textResourceShouldFail;
|
||||
|
||||
@@ -34,15 +32,10 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
|
||||
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
|
||||
React = require('react');
|
||||
ReactTestRenderer = require('react-test-renderer');
|
||||
// JestReact = require('jest-react');
|
||||
ReactCache = require('react-cache');
|
||||
|
||||
Suspense = React.Suspense;
|
||||
|
||||
function invalidateCache() {
|
||||
cache = ReactCache.createCache(invalidateCache);
|
||||
}
|
||||
invalidateCache();
|
||||
TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
|
||||
let listeners = null;
|
||||
let status = 'pending';
|
||||
@@ -98,7 +91,7 @@ function runPlaceholderTests(suiteLabel, loadReactNoop) {
|
||||
function AsyncText(props) {
|
||||
const text = props.text;
|
||||
try {
|
||||
TextResource.read(cache, [props.text, props.ms]);
|
||||
TextResource.read([props.text, props.ms]);
|
||||
ReactTestRenderer.unstable_yield(text);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
|
||||
+3
-28
@@ -7,7 +7,6 @@ let Suspense;
|
||||
let StrictMode;
|
||||
let ConcurrentMode;
|
||||
|
||||
let cache;
|
||||
let TextResource;
|
||||
let textResourceShouldFail;
|
||||
|
||||
@@ -28,10 +27,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
StrictMode = React.StrictMode;
|
||||
ConcurrentMode = React.unstable_ConcurrentMode;
|
||||
|
||||
function invalidateCache() {
|
||||
cache = ReactCache.createCache(invalidateCache);
|
||||
}
|
||||
invalidateCache();
|
||||
TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
|
||||
return new Promise((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
@@ -80,7 +75,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
function AsyncText(props) {
|
||||
const text = props.text;
|
||||
try {
|
||||
TextResource.read(cache, [props.text, props.ms]);
|
||||
TextResource.read([props.text, props.ms]);
|
||||
ReactNoop.yield(text);
|
||||
return <span prop={text} />;
|
||||
} catch (promise) {
|
||||
@@ -278,16 +273,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Caught error: Failed to load: Result'),
|
||||
]);
|
||||
|
||||
// Reset the error boundary and cache, and try again.
|
||||
errorBoundary.current.reset();
|
||||
cache.invalidate();
|
||||
|
||||
expect(ReactNoop.flush()).toEqual(['Suspend! [Result]', 'Loading...']);
|
||||
ReactNoop.expire(1000);
|
||||
await advanceTimers(1000);
|
||||
expect(ReactNoop.flush()).toEqual(['Promise resolved [Result]', 'Result']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Result')]);
|
||||
});
|
||||
|
||||
it('retries on error after falling back to a placeholder', async () => {
|
||||
@@ -346,16 +331,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Caught error: Failed to load: Result'),
|
||||
]);
|
||||
|
||||
// Reset the error boundary and cache, and try again.
|
||||
errorBoundary.current.reset();
|
||||
cache.invalidate();
|
||||
|
||||
expect(ReactNoop.flush()).toEqual(['Suspend! [Result]', 'Loading...']);
|
||||
ReactNoop.expire(3000);
|
||||
await advanceTimers(3000);
|
||||
expect(ReactNoop.flush()).toEqual(['Promise resolved [Result]', 'Result']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('Result')]);
|
||||
});
|
||||
|
||||
it('can update at a higher priority while in a suspended state', async () => {
|
||||
@@ -1339,7 +1314,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
const text = props.text;
|
||||
ReactNoop.yield('constructor');
|
||||
try {
|
||||
TextResource.read(cache, [props.text, props.ms]);
|
||||
TextResource.read([props.text, props.ms]);
|
||||
this.state = {text};
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
@@ -1444,7 +1419,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
const text = this.props.text;
|
||||
const ms = this.props.ms;
|
||||
try {
|
||||
TextResource.read(cache, [text, ms]);
|
||||
TextResource.read([text, ms]);
|
||||
ReactNoop.yield(text);
|
||||
return <span prop={text} />;
|
||||
} catch (promise) {
|
||||
|
||||
@@ -13,11 +13,16 @@
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactNoop;
|
||||
let ReactCache;
|
||||
let ReactTestRenderer;
|
||||
let advanceTimeBy;
|
||||
let SchedulerTracing;
|
||||
let mockNow;
|
||||
let AdvanceTime;
|
||||
let AsyncText;
|
||||
let Text;
|
||||
let TextResource;
|
||||
let resourcePromise;
|
||||
|
||||
function loadModules({
|
||||
enableProfilerTimer = true,
|
||||
@@ -40,6 +45,7 @@ function loadModules({
|
||||
|
||||
React = require('react');
|
||||
SchedulerTracing = require('scheduler/tracing');
|
||||
ReactCache = require('react-cache');
|
||||
|
||||
if (useNoopRenderer) {
|
||||
ReactNoop = require('react-noop-renderer');
|
||||
@@ -68,6 +74,46 @@ function loadModules({
|
||||
return this.props.children || null;
|
||||
}
|
||||
};
|
||||
|
||||
resourcePromise = null;
|
||||
|
||||
function yieldForRenderer(value) {
|
||||
if (ReactNoop) {
|
||||
ReactNoop.yield(value);
|
||||
} else {
|
||||
ReactTestRenderer.unstable_yield(value);
|
||||
}
|
||||
}
|
||||
|
||||
TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
|
||||
resourcePromise = new Promise((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
yieldForRenderer(`Promise resolved [${text}]`);
|
||||
resolve(text);
|
||||
}, ms),
|
||||
);
|
||||
return resourcePromise;
|
||||
}, ([text, ms]) => text);
|
||||
|
||||
AsyncText = ({ms, text}) => {
|
||||
try {
|
||||
TextResource.read([text, ms]);
|
||||
yieldForRenderer(`AsyncText [${text}]`);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
yieldForRenderer(`Suspend [${text}]`);
|
||||
} else {
|
||||
yieldForRenderer(`Error [${text}]`);
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
};
|
||||
|
||||
Text = ({text}) => {
|
||||
yieldForRenderer(`Text [${text}]`);
|
||||
return text;
|
||||
};
|
||||
}
|
||||
|
||||
const mockDevToolsForTest = () => {
|
||||
@@ -2145,12 +2191,6 @@ describe('Profiler', () => {
|
||||
});
|
||||
|
||||
describe('suspense', () => {
|
||||
let AsyncText;
|
||||
let Text;
|
||||
let TextResource;
|
||||
let cache;
|
||||
let resourcePromise;
|
||||
|
||||
function awaitableAdvanceTimers(ms) {
|
||||
jest.advanceTimersByTime(ms);
|
||||
// Wait until the end of the current tick
|
||||
@@ -2159,54 +2199,6 @@ describe('Profiler', () => {
|
||||
});
|
||||
}
|
||||
|
||||
function yieldForRenderer(value) {
|
||||
if (ReactNoop) {
|
||||
ReactNoop.yield(value);
|
||||
} else {
|
||||
ReactTestRenderer.unstable_yield(value);
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
const ReactCache = require('react-cache');
|
||||
function invalidateCache() {
|
||||
cache = ReactCache.createCache(invalidateCache);
|
||||
}
|
||||
invalidateCache();
|
||||
|
||||
resourcePromise = null;
|
||||
|
||||
TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
|
||||
resourcePromise = new Promise((resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
yieldForRenderer(`Promise resolved [${text}]`);
|
||||
resolve(text);
|
||||
}, ms),
|
||||
);
|
||||
return resourcePromise;
|
||||
}, ([text, ms]) => text);
|
||||
|
||||
AsyncText = ({ms, text}) => {
|
||||
try {
|
||||
TextResource.read(cache, [text, ms]);
|
||||
yieldForRenderer(`AsyncText [${text}]`);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
yieldForRenderer(`Suspend [${text}]`);
|
||||
} else {
|
||||
yieldForRenderer(`Error [${text}]`);
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
};
|
||||
|
||||
Text = ({text}) => {
|
||||
yieldForRenderer(`Text [${text}]`);
|
||||
return text;
|
||||
};
|
||||
});
|
||||
|
||||
it('traces both the temporary placeholder and the finished render for an interaction', async () => {
|
||||
loadModulesForTracing({useNoopRenderer: true});
|
||||
|
||||
@@ -2352,7 +2344,7 @@ describe('Profiler', () => {
|
||||
|
||||
render() {
|
||||
const {ms, text} = this.props;
|
||||
TextResource.read(cache, [text, ms]);
|
||||
TextResource.read([text, ms]);
|
||||
return <span prop={text}>{this.state.hasMounted}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,6 @@ function loadModules() {
|
||||
|
||||
describe('ProfilerDOM', () => {
|
||||
let TextResource;
|
||||
let cache;
|
||||
let resourcePromise;
|
||||
let onInteractionScheduledWorkCompleted;
|
||||
let onInteractionTraced;
|
||||
@@ -81,8 +80,6 @@ describe('ProfilerDOM', () => {
|
||||
onWorkStopped: () => {},
|
||||
});
|
||||
|
||||
cache = ReactCache.createCache(() => {});
|
||||
|
||||
resourcePromise = null;
|
||||
|
||||
TextResource = ReactCache.unstable_createResource(([text, ms = 0]) => {
|
||||
@@ -101,7 +98,7 @@ describe('ProfilerDOM', () => {
|
||||
});
|
||||
|
||||
const AsyncText = ({ms, text}) => {
|
||||
TextResource.read(cache, [text, ms]);
|
||||
TextResource.read([text, ms]);
|
||||
return text;
|
||||
};
|
||||
|
||||
|
||||
@@ -377,7 +377,7 @@ const bundles = [
|
||||
moduleType: ISOMORPHIC,
|
||||
entry: 'react-cache',
|
||||
global: 'ReactCache',
|
||||
externals: ['react'],
|
||||
externals: ['react', 'scheduler'],
|
||||
},
|
||||
|
||||
/******* createComponentWithSubscriptions (experimental) *******/
|
||||
|
||||
Reference in New Issue
Block a user