2018-02-16 08:38:15 +08:00
|
|
|
/**
|
2018-09-08 06:11:23 +08:00
|
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
2018-02-16 08:38:15 +08:00
|
|
|
*
|
|
|
|
|
* This source code is licensed under the MIT license found in the
|
|
|
|
|
* LICENSE file in the root directory of this source tree.
|
|
|
|
|
*
|
|
|
|
|
* @flow
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import React from 'react';
|
2018-07-17 05:31:59 +08:00
|
|
|
import warningWithoutStack from 'shared/warningWithoutStack';
|
2018-02-16 08:38:15 +08:00
|
|
|
|
|
|
|
|
function noop() {}
|
|
|
|
|
|
|
|
|
|
const Empty = 0;
|
|
|
|
|
const Pending = 1;
|
|
|
|
|
const Resolved = 2;
|
|
|
|
|
const Rejected = 3;
|
|
|
|
|
|
2018-05-31 06:31:41 +08:00
|
|
|
type EmptyRecord<K> = {|
|
2018-02-16 08:38:15 +08:00
|
|
|
status: 0,
|
|
|
|
|
suspender: null,
|
2018-05-31 04:12:29 +08:00
|
|
|
key: K,
|
2018-02-16 08:38:15 +08:00
|
|
|
value: null,
|
|
|
|
|
error: null,
|
2018-05-31 06:31:41 +08:00
|
|
|
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,
|
|
|
|
|
*/
|
2018-02-16 08:38:15 +08:00
|
|
|
|};
|
|
|
|
|
|
2018-05-31 04:12:29 +08:00
|
|
|
type PendingRecord<K, V> = {|
|
2018-02-16 08:38:15 +08:00
|
|
|
status: 1,
|
2018-05-31 06:31:41 +08:00
|
|
|
suspender: Promise<V>,
|
2018-05-31 04:12:29 +08:00
|
|
|
key: K,
|
2018-02-16 08:38:15 +08:00
|
|
|
value: null,
|
|
|
|
|
error: null,
|
2018-05-31 06:31:41 +08:00
|
|
|
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,
|
|
|
|
|
*/
|
2018-02-16 08:38:15 +08:00
|
|
|
|};
|
|
|
|
|
|
2018-05-31 04:12:29 +08:00
|
|
|
type ResolvedRecord<K, V> = {|
|
2018-02-16 08:38:15 +08:00
|
|
|
status: 2,
|
|
|
|
|
suspender: null,
|
2018-05-31 04:12:29 +08:00
|
|
|
key: K,
|
2018-02-16 08:38:15 +08:00
|
|
|
value: V,
|
|
|
|
|
error: null,
|
2018-05-31 06:31:41 +08:00
|
|
|
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,
|
|
|
|
|
*/
|
2018-02-16 08:38:15 +08:00
|
|
|
|};
|
|
|
|
|
|
2018-05-31 06:31:41 +08:00
|
|
|
type RejectedRecord<K> = {|
|
2018-02-16 08:38:15 +08:00
|
|
|
status: 3,
|
|
|
|
|
suspender: null,
|
2018-05-31 04:12:29 +08:00
|
|
|
key: K,
|
2018-02-16 08:38:15 +08:00
|
|
|
value: null,
|
|
|
|
|
error: Error,
|
2018-05-31 06:31:41 +08:00
|
|
|
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,
|
|
|
|
|
*/
|
2018-02-16 08:38:15 +08:00
|
|
|
|};
|
|
|
|
|
|
2018-05-31 04:12:29 +08:00
|
|
|
type Record<K, V> =
|
2018-05-31 06:31:41 +08:00
|
|
|
| EmptyRecord<K>
|
2018-05-31 04:12:29 +08:00
|
|
|
| PendingRecord<K, V>
|
|
|
|
|
| ResolvedRecord<K, V>
|
2018-05-31 06:31:41 +08:00
|
|
|
| RejectedRecord<K>;
|
2018-05-31 04:12:29 +08:00
|
|
|
|
|
|
|
|
type RecordCache<K, V> = {|
|
|
|
|
|
map: Map<K, Record<K, V>>,
|
|
|
|
|
head: Record<K, V> | null,
|
|
|
|
|
tail: Record<K, V> | null,
|
2018-05-31 06:31:41 +08:00
|
|
|
size: number,
|
2018-05-31 04:12:29 +08:00
|
|
|
|};
|
2018-02-16 08:38:15 +08:00
|
|
|
|
|
|
|
|
// TODO: How do you express this type with Flow?
|
2018-05-31 04:12:29 +08:00
|
|
|
type ResourceMap = Map<any, RecordCache<any, any>>;
|
2018-02-16 08:38:15 +08:00
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let CACHE_TYPE;
|
|
|
|
|
if (__DEV__) {
|
|
|
|
|
CACHE_TYPE = 0xcac4e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let isCache;
|
|
|
|
|
if (__DEV__) {
|
|
|
|
|
isCache = value =>
|
|
|
|
|
value !== null &&
|
|
|
|
|
typeof value === 'object' &&
|
|
|
|
|
value.$$typeof === CACHE_TYPE;
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-31 04:12:29 +08:00
|
|
|
// TODO: Make this configurable per resource
|
|
|
|
|
const MAX_SIZE = 500;
|
|
|
|
|
const PAGE_SIZE = 50;
|
|
|
|
|
|
2018-05-31 06:31:41 +08:00
|
|
|
function createRecord<K>(key: K): EmptyRecord<K> {
|
2018-05-31 04:12:29 +08:00
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-16 08:38:15 +08:00
|
|
|
export function createCache(invalidator: () => mixed): Cache {
|
2018-05-31 04:12:29 +08:00
|
|
|
const resourceMap: ResourceMap = new Map();
|
2018-02-16 08:38:15 +08:00
|
|
|
|
2018-05-31 06:31:41 +08:00
|
|
|
function accessRecord<K, V>(resourceType: any, key: K): Record<K, V> {
|
2018-02-16 08:38:15 +08:00
|
|
|
if (__DEV__) {
|
2018-07-17 05:31:59 +08:00
|
|
|
warningWithoutStack(
|
2018-02-16 08:38:15 +08:00
|
|
|
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,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-05-31 04:12:29 +08:00
|
|
|
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;
|
2018-02-16 08:38:15 +08:00
|
|
|
}
|
2018-05-31 04:12:29 +08:00
|
|
|
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;
|
2018-02-16 08:38:15 +08:00
|
|
|
} else {
|
2018-05-31 04:12:29 +08:00
|
|
|
recordCache.tail = newHead;
|
2018-02-16 08:38:15 +08:00
|
|
|
}
|
2018-05-31 04:12:29 +08:00
|
|
|
recordCache.size += 1;
|
2018-02-16 08:38:15 +08:00
|
|
|
|
2018-05-31 04:12:29 +08:00
|
|
|
return newHead;
|
2018-02-16 08:38:15 +08:00
|
|
|
}
|
|
|
|
|
|
2018-05-31 06:31:41 +08:00
|
|
|
function load<K, V>(emptyRecord: EmptyRecord<K>, suspender: Promise<V>) {
|
|
|
|
|
const pendingRecord: PendingRecord<K, V> = (emptyRecord: any);
|
2018-02-16 08:38:15 +08:00
|
|
|
pendingRecord.status = Pending;
|
|
|
|
|
pendingRecord.suspender = suspender;
|
|
|
|
|
suspender.then(
|
|
|
|
|
value => {
|
|
|
|
|
// Resource loaded successfully.
|
2018-05-31 06:31:41 +08:00
|
|
|
const resolvedRecord: ResolvedRecord<K, V> = (pendingRecord: any);
|
2018-02-16 08:38:15 +08:00
|
|
|
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.
|
2018-05-31 06:31:41 +08:00
|
|
|
const rejectedRecord: RejectedRecord<K> = (pendingRecord: any);
|
2018-02-16 08:38:15 +08:00
|
|
|
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 {
|
2018-05-31 06:31:41 +08:00
|
|
|
const record: Record<K, V> = accessRecord(resourceType, key);
|
2018-02-16 08:38:15 +08:00
|
|
|
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 {
|
2018-05-31 06:31:41 +08:00
|
|
|
const record: Record<K, V> = accessRecord(resourceType, key);
|
2018-02-16 08:38:15 +08:00
|
|
|
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) => {
|
2018-07-17 05:31:59 +08:00
|
|
|
warningWithoutStack(
|
2018-02-16 08:38:15 +08:00
|
|
|
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 ' +
|
2018-10-24 04:55:37 +08:00
|
|
|
'function as the second argument to unstable_createResource().',
|
2018-02-16 08:38:15 +08:00
|
|
|
methodName,
|
|
|
|
|
key,
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type primitive = string | number | boolean | void | null;
|
|
|
|
|
|
2018-03-01 02:17:03 +08:00
|
|
|
type Resource<K, V> = {|
|
|
|
|
|
read(Cache, K): V,
|
2018-02-16 08:38:15 +08:00
|
|
|
preload(cache: Cache, key: K): void,
|
2018-03-01 02:17:03 +08:00
|
|
|
|};
|
2018-02-16 08:38:15 +08:00
|
|
|
|
|
|
|
|
// These declarations are used to express function overloading. I wish there
|
|
|
|
|
// were a more elegant way to do this in the function definition itself.
|
|
|
|
|
|
|
|
|
|
// Primitive keys do not request a hash function.
|
2018-10-24 04:55:37 +08:00
|
|
|
declare function unstable_createResource<V, K: primitive, H: primitive>(
|
2018-02-16 08:38:15 +08:00
|
|
|
loadResource: (K) => Promise<V>,
|
|
|
|
|
hash?: (K) => H,
|
|
|
|
|
): Resource<K, V>;
|
|
|
|
|
|
|
|
|
|
// Non-primitive keys *do* require a hash function.
|
|
|
|
|
// eslint-disable-next-line no-redeclare
|
2018-10-24 04:55:37 +08:00
|
|
|
declare function unstable_createResource<V, K: mixed, H: primitive>(
|
2018-02-16 08:38:15 +08:00
|
|
|
loadResource: (K) => Promise<V>,
|
|
|
|
|
hash: (K) => H,
|
|
|
|
|
): Resource<K, V>;
|
|
|
|
|
|
|
|
|
|
// eslint-disable-next-line no-redeclare
|
2018-10-24 04:55:37 +08:00
|
|
|
export function unstable_createResource<V, K, H: primitive>(
|
2018-02-16 08:38:15 +08:00
|
|
|
loadResource: K => Promise<V>,
|
|
|
|
|
hash: K => H,
|
|
|
|
|
): Resource<K, V> {
|
2018-03-01 02:17:03 +08:00
|
|
|
const resource = {
|
|
|
|
|
read(cache, key) {
|
2018-02-16 08:38:15 +08:00
|
|
|
if (__DEV__) {
|
2018-07-17 05:31:59 +08:00
|
|
|
warningWithoutStack(
|
2018-03-01 02:17:03 +08:00
|
|
|
isCache(cache),
|
|
|
|
|
'read(): The first argument must be a cache. Instead received: %s',
|
|
|
|
|
cache,
|
|
|
|
|
);
|
2018-02-16 08:38:15 +08:00
|
|
|
}
|
2018-03-01 02:17:03 +08:00
|
|
|
if (hash === undefined) {
|
|
|
|
|
if (__DEV__) {
|
|
|
|
|
warnIfNonPrimitiveKey(key, 'read');
|
|
|
|
|
}
|
|
|
|
|
return cache.read(resource, key, loadResource, key);
|
|
|
|
|
}
|
|
|
|
|
const hashedKey = hash(key);
|
|
|
|
|
return cache.read(resource, hashedKey, loadResource, key);
|
|
|
|
|
},
|
|
|
|
|
preload(cache, key) {
|
2018-02-16 08:38:15 +08:00
|
|
|
if (__DEV__) {
|
2018-07-17 05:31:59 +08:00
|
|
|
warningWithoutStack(
|
2018-03-01 02:17:03 +08:00
|
|
|
isCache(cache),
|
|
|
|
|
'preload(): The first argument must be a cache. Instead received: %s',
|
|
|
|
|
cache,
|
|
|
|
|
);
|
2018-02-16 08:38:15 +08:00
|
|
|
}
|
2018-03-01 02:17:03 +08:00
|
|
|
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);
|
|
|
|
|
},
|
2018-02-16 08:38:15 +08:00
|
|
|
};
|
2018-03-01 02:17:03 +08:00
|
|
|
return resource;
|
2018-02-16 08:38:15 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Global cache has no eviction policy (except for, ya know, a browser refresh).
|
|
|
|
|
const globalCache = createCache(noop);
|
2018-10-01 23:07:40 +08:00
|
|
|
export const ReactCache = React.createContext(globalCache);
|