/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {ReactContext, Thenable} from 'shared/ReactTypes';
import * as React from 'react';
import {createContext} from 'react';
// TODO (cache) Remove this cache; it is outdated and will not work with newer APIs like startTransition.
// Cache implementation was forked from the React repo:
// https://github.com/facebook/react/blob/main/packages/react-cache/src/ReactCache.js
//
// This cache is simpler than react-cache in that:
// 1. Individual items don't need to be invalidated.
// Profiling data is invalidated as a whole.
// 2. We didn't need the added overhead of an LRU cache.
// The size of this cache is bounded by how many renders were profiled,
// and it will be fully reset between profiling sessions.
export type {Thenable};
interface Suspender {
then(resolve: () => mixed, reject: () => mixed): mixed;
}
type PendingResult = {
status: 0,
value: Suspender,
};
type ResolvedResult<Value> = {
status: 1,
value: Value,
};
type RejectedResult = {
status: 2,
value: mixed,
};
type Result<Value> = PendingResult | ResolvedResult<Value> | RejectedResult;
export type Resource<Input, Key, Value> = {
clear(): void,
invalidate(Key): void,
read(Input): Value,
preload(Input): void,
write(Key, Value): void,
...
};
const Pending = 0;
const Resolved = 1;
const Rejected = 2;
const ReactCurrentDispatcher = (React: any)
.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher;
function readContext(Context: ReactContext<null>) {
const dispatcher = ReactCurrentDispatcher.current;
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);
}
const CacheContext = createContext(null);
type Config = {useWeakMap?: boolean, ...};
const entries: Map<
Resource<any, any, any>,
Map<any, any> | WeakMap<any, any>,
> = new Map();
const resourceConfigs: Map<Resource<any, any, any>, Config> = new Map();
function getEntriesForResource(
resource: any,
): Map<any, any> | WeakMap<any, any> {
let entriesForResource: Map<any, any> | WeakMap<any, any> = ((entries.get(
resource,
): any): Map<any, any>);
if (entriesForResource === undefined) {
const config = resourceConfigs.get(resource);
entriesForResource =
config !== undefined && config.useWeakMap ? new WeakMap() : new Map();
entries.set(resource, entriesForResource);
}
return entriesForResource;
}
function accessResult<Input, Key, Value>(
resource: any,
fetch: Input => Thenable<Value>,
input: Input,
key: Key,
): Result<Value> {
const entriesForResource = getEntriesForResource(resource);
const entry = entriesForResource.get(key);
if (entry === undefined) {
const thenable = fetch(input);
thenable.then(
value => {
if (newResult.status === Pending) {
const resolvedResult: ResolvedResult<Value> = (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,
};
entriesForResource.set(key, newResult);
return newResult;
} else {
return entry;
}
}
export function createResource<Input, Key, Value>(
fetch: Input => Thenable<Value>,
hashInput: Input => Key,
config?: Config = {},
): Resource<Input, Key, Value> {
const resource = {
clear(): void {
entries.delete(resource);
},
invalidate(key: Key): void {
const entriesForResource = getEntriesForResource(resource);
entriesForResource.delete(key);
},
read(input: Input): Value {
// Prevent access outside of render.
readContext(CacheContext);
const key = hashInput(input);
const result: Result<Value> = accessResult(resource, fetch, input, key);
switch (result.status) {
case Pending: {
const suspender = result.value;
throw suspender;
}
case Resolved: {
const value = result.value;
return value;
}
case Rejected: {
const error = result.value;
throw error;
}
default:
// Should be unreachable
return (undefined: any);
}
},
preload(input: Input): void {
// Prevent access outside of render.
readContext(CacheContext);
const key = hashInput(input);
accessResult(resource, fetch, input, key);
},
write(key: Key, value: Value): void {
const entriesForResource = getEntriesForResource(resource);
const resolvedResult: ResolvedResult<Value> = {
status: Resolved,
value,
};
entriesForResource.set(key, resolvedResult);
},
};
resourceConfigs.set(resource, config);
return resource;
}
export function invalidateResources(): void {
entries.clear();
}