Version 1.0.2 mit node_modules Verzeichnis

This commit is contained in:
ISA
2024-10-02 07:58:24 +02:00
parent f353a06b1b
commit 62b6e55a0a
68228 changed files with 4548477 additions and 651 deletions

View File

@@ -0,0 +1,125 @@
/**
* 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 strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {ValueOrUpdater} from '../recoil_values/Recoil_callbackTypes';
import type {RecoilState, RecoilValue} from './Recoil_RecoilValue';
import type {NodeKey, Store, TreeState} from './Recoil_State';
const {loadableWithValue} = require('../adt/Recoil_Loadable');
const {initializeNode} = require('./Recoil_FunctionalCore');
const {DEFAULT_VALUE, getNode} = require('./Recoil_Node');
const {
copyTreeState,
getRecoilValueAsLoadable,
invalidateDownstreams,
writeLoadableToTreeState,
} = require('./Recoil_RecoilValueInterface');
const err = require('recoil-shared/util/Recoil_err');
export interface TransactionInterface {
get: <T>(RecoilValue<T>) => T;
set: <T>(RecoilState<T>, ValueOrUpdater<T>) => void;
reset: <T>(RecoilState<T>) => void;
}
function isAtom<T>(recoilValue: RecoilValue<T>): boolean {
return getNode(recoilValue.key).nodeType === 'atom';
}
class TransactionInterfaceImpl {
_store: Store;
_treeState: TreeState;
_changes: Map<NodeKey, mixed>;
constructor(store: Store, treeState: TreeState) {
this._store = store;
this._treeState = treeState;
this._changes = new Map();
}
// Allow destructing
// eslint-disable-next-line fb-www/extra-arrow-initializer
get = <T>(recoilValue: RecoilValue<T>): T => {
if (this._changes.has(recoilValue.key)) {
// $FlowIssue[incompatible-return]
return this._changes.get(recoilValue.key);
}
if (!isAtom(recoilValue)) {
throw err('Reading selectors within atomicUpdate is not supported');
}
const loadable = getRecoilValueAsLoadable(
this._store,
recoilValue,
this._treeState,
);
if (loadable.state === 'hasValue') {
return loadable.contents;
} else if (loadable.state === 'hasError') {
throw loadable.contents;
} else {
throw err(
`Expected Recoil atom ${recoilValue.key} to have a value, but it is in a loading state.`,
);
}
};
// Allow destructing
// eslint-disable-next-line fb-www/extra-arrow-initializer
set = <T>(
recoilState: RecoilState<T>,
valueOrUpdater: ValueOrUpdater<T>,
): void => {
if (!isAtom(recoilState)) {
throw err('Setting selectors within atomicUpdate is not supported');
}
if (typeof valueOrUpdater === 'function') {
const current = this.get(recoilState);
this._changes.set(recoilState.key, (valueOrUpdater: any)(current)); // flowlint-line unclear-type:off
} else {
// Initialize atom and run effects if not initialized yet
initializeNode(this._store, recoilState.key, 'set');
this._changes.set(recoilState.key, valueOrUpdater);
}
};
// Allow destructing
// eslint-disable-next-line fb-www/extra-arrow-initializer
reset = <T>(recoilState: RecoilState<T>): void => {
this.set(recoilState, DEFAULT_VALUE);
};
newTreeState_INTERNAL(): TreeState {
if (this._changes.size === 0) {
return this._treeState;
}
const newState = copyTreeState(this._treeState);
for (const [k, v] of this._changes) {
writeLoadableToTreeState(newState, k, loadableWithValue(v));
}
invalidateDownstreams(this._store, newState);
return newState;
}
}
function atomicUpdater(store: Store): ((TransactionInterface) => void) => void {
return fn => {
store.replaceState(treeState => {
const changeset = new TransactionInterfaceImpl(store, treeState);
fn(changeset);
return changeset.newTreeState_INTERNAL();
});
};
}
module.exports = {atomicUpdater};

62
node_modules/recoil/cjs/core/Recoil_Batching.js.flow generated vendored Normal file
View File

@@ -0,0 +1,62 @@
/**
* 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 strict-local
* @format
* @oncall recoil
*/
const {batchStart} = require('../core/Recoil_RecoilValueInterface');
const {
unstable_batchedUpdates,
} = require('recoil-shared/util/Recoil_ReactBatchedUpdates');
// flowlint-next-line unclear-type:off
type Callback = () => any;
type Batcher = (callback: Callback) => void;
/*
* During SSR, unstable_batchedUpdates may be undefined so this
* falls back to a basic function that executes the batch
*/
let batcher: Batcher = unstable_batchedUpdates || (batchFn => batchFn());
/**
* Sets the provided batcher function as the batcher function used by Recoil.
*
* Set the batcher to a custom batcher for your renderer,
* if you use a renderer other than React DOM or React Native.
*/
const setBatcher: Batcher => void = (newBatcher: Batcher) => {
batcher = newBatcher;
};
/**
* Returns the current batcher function.
*/
const getBatcher: () => Batcher = () => batcher;
/**
* Calls the current batcher function and passes the
* provided callback function.
*/
const batchUpdates: Callback => void = (callback: Callback) => {
batcher(() => {
let batchEnd = () => undefined;
try {
batchEnd = batchStart();
callback();
} finally {
batchEnd();
}
});
};
module.exports = {
getBatcher,
setBatcher,
batchUpdates,
};

View File

@@ -0,0 +1,263 @@
/**
* 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 strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Loadable} from '../adt/Recoil_Loadable';
import type {DefaultValue, Trigger} from './Recoil_Node';
import type {RecoilValue} from './Recoil_RecoilValue';
import type {RetainedBy} from './Recoil_RetainedBy';
import type {AtomWrites, NodeKey, Store, TreeState} from './Recoil_State';
const {getNode, getNodeMaybe, recoilValuesForKeys} = require('./Recoil_Node');
const {RetentionZone} = require('./Recoil_RetentionZone');
const {setByAddingToSet} = require('recoil-shared/util/Recoil_CopyOnWrite');
const filterIterable = require('recoil-shared/util/Recoil_filterIterable');
const gkx = require('recoil-shared/util/Recoil_gkx');
const lazyProxy = require('recoil-shared/util/Recoil_lazyProxy');
const mapIterable = require('recoil-shared/util/Recoil_mapIterable');
// flowlint-next-line unclear-type:off
const emptySet: $ReadOnlySet<any> = Object.freeze(new Set());
class ReadOnlyRecoilValueError extends Error {}
function initializeRetentionForNode(
store: Store,
nodeKey: NodeKey,
retainedBy: RetainedBy,
): () => void {
if (!gkx('recoil_memory_managament_2020')) {
return () => undefined;
}
const {nodesRetainedByZone} = store.getState().retention;
function addToZone(zone: RetentionZone) {
let set = nodesRetainedByZone.get(zone);
if (!set) {
nodesRetainedByZone.set(zone, (set = new Set()));
}
set.add(nodeKey);
}
if (retainedBy instanceof RetentionZone) {
addToZone(retainedBy);
} else if (Array.isArray(retainedBy)) {
for (const zone of retainedBy) {
addToZone(zone);
}
}
return () => {
if (!gkx('recoil_memory_managament_2020')) {
return;
}
const {retention} = store.getState();
function deleteFromZone(zone: RetentionZone) {
const set = retention.nodesRetainedByZone.get(zone);
set?.delete(nodeKey);
if (set && set.size === 0) {
retention.nodesRetainedByZone.delete(zone);
}
}
if (retainedBy instanceof RetentionZone) {
deleteFromZone(retainedBy);
} else if (Array.isArray(retainedBy)) {
for (const zone of retainedBy) {
deleteFromZone(zone);
}
}
};
}
function initializeNodeIfNewToStore(
store: Store,
treeState: TreeState,
key: NodeKey,
trigger: Trigger,
): void {
const storeState = store.getState();
if (storeState.nodeCleanupFunctions.has(key)) {
return;
}
const node = getNode(key);
const retentionCleanup = initializeRetentionForNode(
store,
key,
node.retainedBy,
);
const nodeCleanup = node.init(store, treeState, trigger);
storeState.nodeCleanupFunctions.set(key, () => {
nodeCleanup();
retentionCleanup();
});
}
function initializeNode(store: Store, key: NodeKey, trigger: Trigger): void {
initializeNodeIfNewToStore(store, store.getState().currentTree, key, trigger);
}
function cleanUpNode(store: Store, key: NodeKey) {
const state = store.getState();
state.nodeCleanupFunctions.get(key)?.();
state.nodeCleanupFunctions.delete(key);
}
// Get the current value loadable of a node and update the state.
// Update dependencies and subscriptions for selectors.
// Update saved value validation for atoms.
function getNodeLoadable<T>(
store: Store,
state: TreeState,
key: NodeKey,
): Loadable<T> {
initializeNodeIfNewToStore(store, state, key, 'get');
return getNode(key).get(store, state);
}
// Peek at the current value loadable for a node without any evaluation or state change
function peekNodeLoadable<T>(
store: Store,
state: TreeState,
key: NodeKey,
): ?Loadable<T> {
return getNode(key).peek(store, state);
}
// Write value directly to state bypassing the Node interface as the node
// definitions may not have been loaded yet when processing the initial snapshot.
function setUnvalidatedAtomValue_DEPRECATED<T>(
state: TreeState,
key: NodeKey,
newValue: T,
): TreeState {
const node = getNodeMaybe(key);
node?.invalidate?.(state);
return {
...state,
atomValues: state.atomValues.clone().delete(key),
nonvalidatedAtoms: state.nonvalidatedAtoms.clone().set(key, newValue),
dirtyAtoms: setByAddingToSet(state.dirtyAtoms, key),
};
}
// Return the discovered dependencies and values to be written by setting
// a node value. (Multiple values may be written due to selectors getting to
// set upstreams; deps may be discovered because of reads in updater functions.)
function setNodeValue<T>(
store: Store,
state: TreeState,
key: NodeKey,
newValue: T | DefaultValue,
): AtomWrites {
const node = getNode(key);
if (node.set == null) {
throw new ReadOnlyRecoilValueError(
`Attempt to set read-only RecoilValue: ${key}`,
);
}
const set = node.set; // so flow doesn't lose the above refinement.
initializeNodeIfNewToStore(store, state, key, 'set');
return set(store, state, newValue);
}
type ComponentInfo = {
name: string,
};
export type RecoilValueInfo<T> = {
loadable: ?Loadable<T>,
isActive: boolean,
isSet: boolean,
isModified: boolean, // TODO report modified selectors
type: 'atom' | 'selector',
deps: Iterable<RecoilValue<mixed>>,
subscribers: {
nodes: Iterable<RecoilValue<mixed>>,
components: Iterable<ComponentInfo>,
},
};
function peekNodeInfo<T>(
store: Store,
state: TreeState,
key: NodeKey,
): RecoilValueInfo<T> {
const storeState = store.getState();
const graph = store.getGraph(state.version);
const type = getNode(key).nodeType;
return lazyProxy(
{
type,
},
{
// $FlowFixMe[underconstrained-implicit-instantiation]
loadable: () => peekNodeLoadable(store, state, key),
isActive: () =>
storeState.knownAtoms.has(key) || storeState.knownSelectors.has(key),
isSet: () => (type === 'selector' ? false : state.atomValues.has(key)),
isModified: () => state.dirtyAtoms.has(key),
// Report current dependencies. If the node hasn't been evaluated, then
// dependencies may be missing based on the current state.
deps: () => recoilValuesForKeys(graph.nodeDeps.get(key) ?? []),
// Reports all "current" subscribers. Evaluating other nodes or
// previous in-progress async evaluations may introduce new subscribers.
subscribers: () => ({
nodes: recoilValuesForKeys(
filterIterable(
getDownstreamNodes(store, state, new Set([key])),
nodeKey => nodeKey !== key,
),
),
components: mapIterable(
storeState.nodeToComponentSubscriptions.get(key)?.values() ?? [],
([name]) => ({name}),
),
}),
},
);
}
// Find all of the recursively dependent nodes
function getDownstreamNodes(
store: Store,
state: TreeState,
keys: $ReadOnlySet<NodeKey> | $ReadOnlyArray<NodeKey>,
): $ReadOnlySet<NodeKey> {
const visitedNodes = new Set<NodeKey>();
const visitingNodes = Array.from(keys);
const graph = store.getGraph(state.version);
for (let key = visitingNodes.pop(); key; key = visitingNodes.pop()) {
visitedNodes.add(key);
const subscribedNodes = graph.nodeToNodeSubscriptions.get(key) ?? emptySet;
for (const downstreamNode of subscribedNodes) {
if (!visitedNodes.has(downstreamNode)) {
visitingNodes.push(downstreamNode);
}
}
}
return visitedNodes;
}
module.exports = {
getNodeLoadable,
peekNodeLoadable,
setNodeValue,
initializeNode,
cleanUpNode,
setUnvalidatedAtomValue_DEPRECATED,
peekNodeInfo,
getDownstreamNodes,
};

134
node_modules/recoil/cjs/core/Recoil_Graph.js.flow generated vendored Normal file
View File

@@ -0,0 +1,134 @@
/**
* 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 strict
* @format
* @oncall recoil
*/
'use strict';
import type {Graph} from './Recoil_GraphTypes';
import type {NodeKey, StateID} from './Recoil_Keys';
import type {Store} from './Recoil_State';
const differenceSets = require('recoil-shared/util/Recoil_differenceSets');
const mapMap = require('recoil-shared/util/Recoil_mapMap');
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
function makeGraph(): Graph {
return {
nodeDeps: new Map(),
nodeToNodeSubscriptions: new Map(),
};
}
function cloneGraph(graph: Graph): Graph {
return {
nodeDeps: mapMap(graph.nodeDeps, s => new Set(s)),
nodeToNodeSubscriptions: mapMap(
graph.nodeToNodeSubscriptions,
s => new Set(s),
),
};
}
// Note that this overwrites the deps of existing nodes, rather than unioning
// the new deps with the old deps.
function mergeDepsIntoGraph(
key: NodeKey,
newDeps: $ReadOnlySet<NodeKey>,
graph: Graph,
// If olderGraph is given then we will not overwrite changes made to the given
// graph compared with olderGraph:
olderGraph?: Graph,
): void {
const {nodeDeps, nodeToNodeSubscriptions} = graph;
const oldDeps = nodeDeps.get(key);
if (oldDeps && olderGraph && oldDeps !== olderGraph.nodeDeps.get(key)) {
return;
}
// Update nodeDeps:
nodeDeps.set(key, newDeps);
// Add new deps to nodeToNodeSubscriptions:
const addedDeps =
oldDeps == null ? newDeps : differenceSets(newDeps, oldDeps);
for (const dep of addedDeps) {
if (!nodeToNodeSubscriptions.has(dep)) {
nodeToNodeSubscriptions.set(dep, new Set());
}
const existing = nullthrows(nodeToNodeSubscriptions.get(dep));
existing.add(key);
}
// Remove removed deps from nodeToNodeSubscriptions:
if (oldDeps) {
const removedDeps = differenceSets(oldDeps, newDeps);
for (const dep of removedDeps) {
if (!nodeToNodeSubscriptions.has(dep)) {
return;
}
const existing = nullthrows(nodeToNodeSubscriptions.get(dep));
existing.delete(key);
if (existing.size === 0) {
nodeToNodeSubscriptions.delete(dep);
}
}
}
}
function saveDepsToStore(
key: NodeKey,
deps: $ReadOnlySet<NodeKey>,
store: Store,
version: StateID,
): void {
const storeState = store.getState();
if (
!(
version === storeState.currentTree.version ||
version === storeState.nextTree?.version ||
version === storeState.previousTree?.version
)
) {
recoverableViolation(
'Tried to save dependencies to a discarded tree',
'recoil',
);
}
// Merge the dependencies discovered into the store's dependency map
// for the version that was read:
const graph = store.getGraph(version);
mergeDepsIntoGraph(key, deps, graph);
// If this version is not the latest version, also write these dependencies
// into later versions if they don't already have their own:
if (version === storeState.previousTree?.version) {
const currentGraph = store.getGraph(storeState.currentTree.version);
mergeDepsIntoGraph(key, deps, currentGraph, graph);
}
if (
version === storeState.previousTree?.version ||
version === storeState.currentTree.version
) {
const nextVersion = storeState.nextTree?.version;
if (nextVersion !== undefined) {
const nextGraph = store.getGraph(nextVersion);
mergeDepsIntoGraph(key, deps, nextGraph, graph);
}
}
}
module.exports = {
cloneGraph,
graph: makeGraph,
saveDepsToStore,
};

27
node_modules/recoil/cjs/core/Recoil_GraphTypes.js.flow generated vendored Normal file
View File

@@ -0,0 +1,27 @@
/**
* 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 strict
* @format
* @oncall recoil
*/
'use strict';
import type {NodeKey} from './Recoil_Keys';
export type Graph = $ReadOnly<{
// TODO rename these properties to be more descriptive and symetric.
// Upstream Node dependencies
// NOTE: if you ever make the sets in nodeDeps mutable you must change the
// logic in mergeDepsIntoGraph() that relies on reference equality
// of these sets in avoiding overwriting newer deps with older ones.
nodeDeps: Map<NodeKey, $ReadOnlySet<NodeKey>>,
// Downstream Node subscriptions
nodeToNodeSubscriptions: Map<NodeKey, Set<NodeKey>>,
}>;
module.exports = ({}: {...});

31
node_modules/recoil/cjs/core/Recoil_Keys.js.flow generated vendored Normal file
View File

@@ -0,0 +1,31 @@
/**
* 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 strict
* @format
* @oncall recoil
*/
'use strict';
export type NodeKey = string;
export opaque type StateID = number;
export opaque type StoreID = number;
export opaque type ComponentID = number;
let nextTreeStateVersion = 0;
const getNextTreeStateVersion: () => StateID = () => nextTreeStateVersion++;
let nextStoreID = 0;
const getNextStoreID: () => StoreID = () => nextStoreID++;
let nextComponentID = 0;
const getNextComponentID: () => ComponentID = () => nextComponentID++;
module.exports = {
getNextTreeStateVersion,
getNextStoreID,
getNextComponentID,
};

205
node_modules/recoil/cjs/core/Recoil_Node.js.flow generated vendored Normal file
View File

@@ -0,0 +1,205 @@
/**
* 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 strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Loadable} from '../adt/Recoil_Loadable';
import type {RecoilValue} from './Recoil_RecoilValue';
import type {RetainedBy} from './Recoil_RetainedBy';
import type {AtomWrites, NodeKey, Store, TreeState} from './Recoil_State';
const {isFastRefreshEnabled} = require('./Recoil_ReactMode');
const RecoilValueClasses = require('./Recoil_RecoilValue');
const expectationViolation = require('recoil-shared/util/Recoil_expectationViolation');
const gkx = require('recoil-shared/util/Recoil_gkx');
const mapIterable = require('recoil-shared/util/Recoil_mapIterable');
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
const RecoilEnv = require('recoil-shared/util/Recoil_RecoilEnv');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
class DefaultValue {}
const DEFAULT_VALUE: DefaultValue = new DefaultValue();
export type PersistenceType = 'none' | 'url';
export type PersistenceInfo = $ReadOnly<{
type: PersistenceType,
backButton?: boolean,
}>;
export type Trigger = 'get' | 'set';
type NodeType = 'atom' | 'selector';
export type ReadOnlyNodeOptions<T> = $ReadOnly<{
key: NodeKey,
nodeType: NodeType,
// Returns the current value without evaluating or modifying state
peek: (Store, TreeState) => ?Loadable<T>,
// Returns the discovered deps and the loadable value of the node
get: (Store, TreeState) => Loadable<T>,
// Informs the node the first time it is used (either ever or since the node was
// last released). Returns a cleanup function for when the store ceases to be or
// the node is released again.
init: (Store, TreeState, Trigger) => () => void,
// Invalidate the cached value stored in the TreeState.
// It is used at the end of each batch for mutated state.
// This does not affect any other caches such as the selector cache.
invalidate: TreeState => void,
// Clear all internal caches for this node. Unlike "invalidate()" this clears
// the selector cache and clears for all possible dependency values.
clearCache?: (Store, TreeState) => void,
shouldRestoreFromSnapshots: boolean,
dangerouslyAllowMutability?: boolean,
persistence_UNSTABLE?: PersistenceInfo,
// True for members of families, since another node can be created later for the
// same parameter value; but false for individual atoms and selectors which have
// a singleton config passed to us only once when they're defined:
shouldDeleteConfigOnRelease?: () => boolean,
retainedBy: RetainedBy,
}>;
export type ReadWriteNodeOptions<T> = $ReadOnly<{
...ReadOnlyNodeOptions<T>,
// Returns the discovered deps and the set of key-value pairs to be written.
// (Deps may be discovered since selectors get an updater function which has
// the ability to read other atoms, which may have deps.)
set: (
store: Store,
state: TreeState,
newValue: T | DefaultValue,
) => AtomWrites,
}>;
type Node<T> = ReadOnlyNodeOptions<T> | ReadWriteNodeOptions<T>;
// flowlint-next-line unclear-type:off
const nodes: Map<string, Node<any>> = new Map();
// flowlint-next-line unclear-type:off
const recoilValues: Map<string, RecoilValue<any>> = new Map();
/* eslint-disable no-redeclare */
declare function registerNode<T>(
node: ReadWriteNodeOptions<T>,
): RecoilValueClasses.RecoilState<T>;
declare function registerNode<T>(
node: ReadOnlyNodeOptions<T>,
): RecoilValueClasses.RecoilValueReadOnly<T>;
function recoilValuesForKeys(
keys: Iterable<NodeKey>,
): Iterable<RecoilValue<mixed>> {
return mapIterable(keys, key => nullthrows(recoilValues.get(key)));
}
function checkForDuplicateAtomKey(key: string): void {
if (nodes.has(key)) {
const message = `Duplicate atom key "${key}". This is a FATAL ERROR in
production. But it is safe to ignore this warning if it occurred because of
hot module replacement.`;
if (__DEV__) {
// TODO Figure this out for open-source
if (!isFastRefreshEnabled()) {
expectationViolation(message, 'recoil');
}
} else {
// @fb-only: recoverableViolation(message, 'recoil');
console.warn(message); // @oss-only
}
}
}
function registerNode<T>(node: Node<T>): RecoilValue<T> {
if (RecoilEnv.RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED) {
checkForDuplicateAtomKey(node.key);
}
nodes.set(node.key, node);
const recoilValue: RecoilValue<T> =
node.set == null
? new RecoilValueClasses.RecoilValueReadOnly(node.key)
: new RecoilValueClasses.RecoilState(node.key);
recoilValues.set(node.key, recoilValue);
return recoilValue;
}
/* eslint-enable no-redeclare */
class NodeMissingError extends Error {}
// flowlint-next-line unclear-type:off
function getNode(key: NodeKey): Node<any> {
const node = nodes.get(key);
if (node == null) {
throw new NodeMissingError(`Missing definition for RecoilValue: "${key}""`);
}
return node;
}
// flowlint-next-line unclear-type:off
function getNodeMaybe(key: NodeKey): void | Node<any> {
return nodes.get(key);
}
const configDeletionHandlers = new Map<NodeKey, () => void>();
function deleteNodeConfigIfPossible(key: NodeKey): void {
if (!gkx('recoil_memory_managament_2020')) {
return;
}
const node = nodes.get(key);
if (node?.shouldDeleteConfigOnRelease?.()) {
nodes.delete(key);
getConfigDeletionHandler(key)?.();
configDeletionHandlers.delete(key);
}
}
function setConfigDeletionHandler(key: NodeKey, fn: void | (() => void)): void {
if (!gkx('recoil_memory_managament_2020')) {
return;
}
if (fn === undefined) {
configDeletionHandlers.delete(key);
} else {
configDeletionHandlers.set(key, fn);
}
}
function getConfigDeletionHandler(key: NodeKey): void | (() => void) {
return configDeletionHandlers.get(key);
}
module.exports = {
nodes,
recoilValues,
registerNode,
getNode,
getNodeMaybe,
deleteNodeConfigIfPossible,
setConfigDeletionHandler,
getConfigDeletionHandler,
recoilValuesForKeys,
NodeMissingError,
DefaultValue,
DEFAULT_VALUE,
};

131
node_modules/recoil/cjs/core/Recoil_ReactMode.js.flow generated vendored Normal file
View File

@@ -0,0 +1,131 @@
/**
* 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 strict-local
* @format
* @oncall recoil
*/
'use strict';
const React = require('react');
const gkx = require('recoil-shared/util/Recoil_gkx');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
export opaque type MutableSource = {};
const createMutableSource: <StoreState, Version>(
{current: StoreState},
() => Version,
) => MutableSource =
// flowlint-next-line unclear-type:off
(React: any).createMutableSource ?? (React: any).unstable_createMutableSource;
const useMutableSource: <StoreState, T>(
MutableSource,
() => T,
(StoreState, () => void) => () => void,
) => T =
// flowlint-next-line unclear-type:off
(React: any).useMutableSource ?? (React: any).unstable_useMutableSource;
// https://github.com/reactwg/react-18/discussions/86
const useSyncExternalStore: <T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
) => T =
// flowlint-next-line unclear-type:off
(React: any).useSyncExternalStore ??
// flowlint-next-line unclear-type:off
(React: any).unstable_useSyncExternalStore;
let ReactRendererVersionMismatchWarnOnce = false;
// Check if the current renderer supports `useSyncExternalStore()`.
// Since React goes through a proxy dispatcher and the current renderer can
// change we can't simply check if `React.useSyncExternalStore()` is defined.
function currentRendererSupportsUseSyncExternalStore(): boolean {
// $FlowFixMe[incompatible-use]
const {ReactCurrentDispatcher, ReactCurrentOwner} =
/* $FlowFixMe[prop-missing] This workaround was approved as a safer mechanism
* to detect if the current renderer supports useSyncExternalStore()
* https://fb.workplace.com/groups/reactjs/posts/9558682330846963/ */
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
const dispatcher =
ReactCurrentDispatcher?.current ?? ReactCurrentOwner.currentDispatcher;
const isUseSyncExternalStoreSupported =
dispatcher.useSyncExternalStore != null;
if (
useSyncExternalStore &&
!isUseSyncExternalStoreSupported &&
!ReactRendererVersionMismatchWarnOnce
) {
ReactRendererVersionMismatchWarnOnce = true;
recoverableViolation(
'A React renderer without React 18+ API support is being used with React 18+.',
'recoil',
);
}
return isUseSyncExternalStoreSupported;
}
type ReactMode =
| 'TRANSITION_SUPPORT'
| 'SYNC_EXTERNAL_STORE'
| 'MUTABLE_SOURCE'
| 'LEGACY';
/**
* mode: The React API and approach to use for syncing state with React
* early: Re-renders from Recoil updates occur:
* 1) earlier
* 2) in sync with React updates in the same batch
* 3) before transaction observers instead of after.
* concurrent: Is the current mode compatible with Concurrent Mode and useTransition()
*/
function reactMode(): {mode: ReactMode, early: boolean, concurrent: boolean} {
// NOTE: This mode is currently broken with some Suspense cases
// see Recoil_selector-test.js
if (gkx('recoil_transition_support')) {
return {mode: 'TRANSITION_SUPPORT', early: true, concurrent: true};
}
if (gkx('recoil_sync_external_store') && useSyncExternalStore != null) {
return {mode: 'SYNC_EXTERNAL_STORE', early: true, concurrent: false};
}
if (
gkx('recoil_mutable_source') &&
useMutableSource != null &&
typeof window !== 'undefined' &&
!window.$disableRecoilValueMutableSource_TEMP_HACK_DO_NOT_USE
) {
return gkx('recoil_suppress_rerender_in_callback')
? {mode: 'MUTABLE_SOURCE', early: true, concurrent: true}
: {mode: 'MUTABLE_SOURCE', early: false, concurrent: false};
}
return gkx('recoil_suppress_rerender_in_callback')
? {mode: 'LEGACY', early: true, concurrent: false}
: {mode: 'LEGACY', early: false, concurrent: false};
}
// TODO Need to figure out if there is a standard/open-source equivalent to see if hot module replacement is happening:
function isFastRefreshEnabled(): boolean {
// @fb-only: const {isAcceptingUpdate} = require('__debug');
// @fb-only: return typeof isAcceptingUpdate === 'function' && isAcceptingUpdate();
return false; // @oss-only
}
module.exports = {
createMutableSource,
useMutableSource,
useSyncExternalStore,
currentRendererSupportsUseSyncExternalStore,
reactMode,
isFastRefreshEnabled,
};

591
node_modules/recoil/cjs/core/Recoil_RecoilRoot.js.flow generated vendored Normal file
View File

@@ -0,0 +1,591 @@
/**
* 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 strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {NodeKey, StateID, StoreID} from './Recoil_Keys';
import type {MutableSource} from './Recoil_ReactMode';
import type {RecoilValue} from './Recoil_RecoilValue';
import type {MutableSnapshot} from './Recoil_Snapshot';
import type {Store, StoreRef, StoreState, TreeState} from './Recoil_State';
// @fb-only: const RecoilusagelogEvent = require('RecoilusagelogEvent');
// @fb-only: const RecoilUsageLogFalcoEvent = require('RecoilUsageLogFalcoEvent');
// @fb-only: const URI = require('URI');
const Queue = require('../adt/Recoil_Queue');
const {
getNextTreeStateVersion,
makeEmptyStoreState,
} = require('../core/Recoil_State');
const {
cleanUpNode,
getDownstreamNodes,
initializeNode,
setNodeValue,
setUnvalidatedAtomValue_DEPRECATED,
} = require('./Recoil_FunctionalCore');
const {graph} = require('./Recoil_Graph');
const {cloneGraph} = require('./Recoil_Graph');
const {getNextStoreID} = require('./Recoil_Keys');
const {createMutableSource, reactMode} = require('./Recoil_ReactMode');
const {applyAtomValueWrites} = require('./Recoil_RecoilValueInterface');
const {releaseScheduledRetainablesNow} = require('./Recoil_Retention');
const {freshSnapshot} = require('./Recoil_Snapshot');
const React = require('react');
const {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} = require('react');
const err = require('recoil-shared/util/Recoil_err');
const expectationViolation = require('recoil-shared/util/Recoil_expectationViolation');
const gkx = require('recoil-shared/util/Recoil_gkx');
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
const unionSets = require('recoil-shared/util/Recoil_unionSets');
const useRefInitOnce = require('recoil-shared/util/Recoil_useRefInitOnce');
type InternalProps = {
initializeState_DEPRECATED?: ({
set: <T>(RecoilValue<T>, T) => void,
setUnvalidatedAtomValues: (Map<string, mixed>) => void,
}) => void,
initializeState?: MutableSnapshot => void,
store_INTERNAL?: Store,
children: React.Node,
};
function notInAContext() {
throw err('This component must be used inside a <RecoilRoot> component.');
}
const defaultStore: Store = Object.freeze({
storeID: getNextStoreID(),
getState: notInAContext,
replaceState: notInAContext,
getGraph: notInAContext,
subscribeToTransactions: notInAContext,
addTransactionMetadata: notInAContext,
});
let stateReplacerIsBeingExecuted: boolean = false;
function startNextTreeIfNeeded(store: Store): void {
if (stateReplacerIsBeingExecuted) {
throw err(
'An atom update was triggered within the execution of a state updater function. State updater functions provided to Recoil must be pure functions.',
);
}
const storeState = store.getState();
if (storeState.nextTree === null) {
if (
gkx('recoil_memory_managament_2020') &&
gkx('recoil_release_on_cascading_update_killswitch_2021')
) {
// If this is a cascading update (that is, rendering due to one state change
// invokes a second state change), we won't have cleaned up retainables yet
// because this normally happens after notifying components. Do it before
// proceeding with the cascading update so that it remains predictable:
if (storeState.commitDepth > 0) {
releaseScheduledRetainablesNow(store);
}
}
const version = storeState.currentTree.version;
const nextVersion = getNextTreeStateVersion();
storeState.nextTree = {
...storeState.currentTree,
version: nextVersion,
stateID: nextVersion,
dirtyAtoms: new Set(),
transactionMetadata: {},
};
storeState.graphsByVersion.set(
nextVersion,
cloneGraph(nullthrows(storeState.graphsByVersion.get(version))),
);
}
}
const AppContext = React.createContext<StoreRef>({current: defaultStore});
const useStoreRef = (): StoreRef => useContext(AppContext);
// $FlowExpectedError[incompatible-call]
const MutableSourceContext = React.createContext<MutableSource>(null);
function useRecoilMutableSource(): MutableSource {
const mutableSource = useContext(MutableSourceContext);
if (mutableSource == null) {
expectationViolation(
'Attempted to use a Recoil hook outside of a <RecoilRoot>. ' +
'<RecoilRoot> must be an ancestor of any component that uses ' +
'Recoil hooks.',
);
}
return mutableSource;
}
function notifyComponents(
store: Store,
storeState: StoreState,
treeState: TreeState,
): void {
const dependentNodes = getDownstreamNodes(
store,
treeState,
treeState.dirtyAtoms,
);
for (const key of dependentNodes) {
const comps = storeState.nodeToComponentSubscriptions.get(key);
if (comps) {
for (const [_subID, [_debugName, callback]] of comps) {
callback(treeState);
}
}
}
}
function sendEndOfBatchNotifications(store: Store) {
const storeState = store.getState();
const treeState = storeState.currentTree;
// Inform transaction subscribers of the transaction:
const dirtyAtoms = treeState.dirtyAtoms;
if (dirtyAtoms.size) {
// Execute Node-specific subscribers before global subscribers
for (const [
key,
subscriptions,
] of storeState.nodeTransactionSubscriptions) {
if (dirtyAtoms.has(key)) {
for (const [_, subscription] of subscriptions) {
subscription(store);
}
}
}
for (const [_, subscription] of storeState.transactionSubscriptions) {
subscription(store);
}
if (!reactMode().early || storeState.suspendedComponentResolvers.size > 0) {
// Notifying components is needed to wake from suspense, even when using
// early rendering.
notifyComponents(store, storeState, treeState);
// Wake all suspended components so the right one(s) can try to re-render.
// We need to wake up components not just when some asynchronous selector
// resolved, but also when changing synchronous values because this may cause
// a selector to change from asynchronous to synchronous, in which case there
// would be no follow-up asynchronous resolution to wake us up.
// TODO OPTIMIZATION Only wake up related downstream components
storeState.suspendedComponentResolvers.forEach(cb => cb());
storeState.suspendedComponentResolvers.clear();
}
}
// Special behavior ONLY invoked by useInterface.
// FIXME delete queuedComponentCallbacks_DEPRECATED when deleting useInterface.
storeState.queuedComponentCallbacks_DEPRECATED.forEach(cb => cb(treeState));
storeState.queuedComponentCallbacks_DEPRECATED.splice(
0,
storeState.queuedComponentCallbacks_DEPRECATED.length,
);
}
function endBatch(store: Store) {
const storeState = store.getState();
storeState.commitDepth++;
try {
const {nextTree} = storeState;
// Ignore commits that are not because of Recoil transactions -- namely,
// because something above RecoilRoot re-rendered:
if (nextTree == null) {
return;
}
// nextTree is now committed -- note that copying and reset occurs when
// a transaction begins, in startNextTreeIfNeeded:
storeState.previousTree = storeState.currentTree;
storeState.currentTree = nextTree;
storeState.nextTree = null;
sendEndOfBatchNotifications(store);
if (storeState.previousTree != null) {
storeState.graphsByVersion.delete(storeState.previousTree.version);
} else {
recoverableViolation(
'Ended batch with no previous state, which is unexpected',
'recoil',
);
}
storeState.previousTree = null;
if (gkx('recoil_memory_managament_2020')) {
// Only release retainables if there were no writes during the end of the
// batch. This avoids releasing something we might be about to use.
if (nextTree == null) {
releaseScheduledRetainablesNow(store);
}
}
} finally {
storeState.commitDepth--;
}
}
/*
* The purpose of the Batcher is to observe when React batches end so that
* Recoil state changes can be batched. Whenever Recoil state changes, we call
* setState on the batcher. Then we wait for that change to be committed, which
* signifies the end of the batch. That's when we respond to the Recoil change.
*/
function Batcher({
setNotifyBatcherOfChange,
}: {
setNotifyBatcherOfChange: (() => void) => void,
}) {
const storeRef = useStoreRef();
const [, setState] = useState(([]: Array<$FlowFixMe>));
// $FlowFixMe[incompatible-call]
setNotifyBatcherOfChange(() => setState({}));
useEffect(() => {
// $FlowFixMe[incompatible-call]
setNotifyBatcherOfChange(() => setState({}));
// If an asynchronous selector resolves after the Batcher is unmounted,
// notifyBatcherOfChange will still be called. An error gets thrown whenever
// setState is called after a component is already unmounted, so this sets
// notifyBatcherOfChange to be a no-op.
return () => {
setNotifyBatcherOfChange(() => {});
};
}, [setNotifyBatcherOfChange]);
useEffect(() => {
// enqueueExecution runs this function immediately; it is only used to
// manipulate the order of useEffects during tests, since React seems to
// call useEffect in an unpredictable order sometimes.
Queue.enqueueExecution('Batcher', () => {
endBatch(storeRef.current);
});
});
return null;
}
if (__DEV__) {
if (typeof window !== 'undefined' && !window.$recoilDebugStates) {
window.$recoilDebugStates = [];
}
}
// When removing this deprecated function, remove stateBySettingRecoilValue
// which will no longer be needed.
function initialStoreState_DEPRECATED(
store: Store,
initializeState: ({
set: <T>(RecoilValue<T>, T) => void,
setUnvalidatedAtomValues: (Map<string, mixed>) => void,
}) => void,
): StoreState {
const initial: StoreState = makeEmptyStoreState();
initializeState({
set: <T>(atom: RecoilValue<T>, value: T) => {
const state = initial.currentTree;
const writes = setNodeValue(store, state, atom.key, value);
const writtenNodes = new Set(writes.keys());
const nonvalidatedAtoms = state.nonvalidatedAtoms.clone();
for (const n of writtenNodes) {
nonvalidatedAtoms.delete(n);
}
initial.currentTree = {
...state,
dirtyAtoms: unionSets(state.dirtyAtoms, writtenNodes),
atomValues: applyAtomValueWrites(state.atomValues, writes), // NB: PLEASE un-export applyAtomValueWrites when deleting this code
nonvalidatedAtoms,
};
},
setUnvalidatedAtomValues: atomValues => {
// FIXME replace this with a mutative loop
atomValues.forEach((v, k) => {
initial.currentTree = setUnvalidatedAtomValue_DEPRECATED(
initial.currentTree,
k,
v,
);
});
},
});
return initial;
}
// Initialize state snapshot for <RecoilRoot> for the initializeState prop.
// Atom effect initialization takes precedence over this prop.
// Any atom effects will be run before initialization, but then cleaned up,
// they are then re-run when used as part of rendering. These semantics are
// compatible with React StrictMode where effects may be re-run multiple times
// but state initialization only happens once the first time.
function initialStoreState(
initializeState: MutableSnapshot => void,
): StoreState {
// Initialize a snapshot and get its store
const snapshot = freshSnapshot(initializeState);
const storeState = snapshot.getStore_INTERNAL().getState();
// Counteract the snapshot auto-release
snapshot.retain();
// Cleanup any effects run during initialization and clear the handlers so
// they will re-initialize if used during rendering. This allows atom effect
// initialization to take precedence over initializeState and be compatible
// with StrictMode semantics.
storeState.nodeCleanupFunctions.forEach(cleanup => cleanup());
storeState.nodeCleanupFunctions.clear();
return storeState;
}
let nextID = 0;
function RecoilRoot_INTERNAL({
initializeState_DEPRECATED,
initializeState,
store_INTERNAL: storeProp, // For use with React "context bridging"
children,
}: InternalProps): React.Node {
// prettier-ignore
// @fb-only: useEffect(() => {
// @fb-only: if (gkx('recoil_usage_logging')) {
// @fb-only: try {
// @fb-only: RecoilUsageLogFalcoEvent.log(() => ({
// @fb-only: type: RecoilusagelogEvent.RECOIL_ROOT_MOUNTED,
// @fb-only: path: URI.getRequestURI().getPath(),
// @fb-only: }));
// @fb-only: } catch {
// @fb-only: recoverableViolation(
// @fb-only: 'Error when logging Recoil Usage event',
// @fb-only: 'recoil',
// @fb-only: );
// @fb-only: }
// @fb-only: }
// @fb-only: }, []);
let storeStateRef: {current: StoreState}; // eslint-disable-line prefer-const
const getGraph = (version: StateID) => {
const graphs = storeStateRef.current.graphsByVersion;
if (graphs.has(version)) {
return nullthrows(graphs.get(version));
}
const newGraph = graph();
graphs.set(version, newGraph);
return newGraph;
};
const subscribeToTransactions = (
callback: Store => void,
key: ?NodeKey,
): ({release: () => void}) => {
if (key == null) {
// Global transaction subscriptions
const {transactionSubscriptions} = storeRef.current.getState();
const id = nextID++;
transactionSubscriptions.set(id, callback);
return {
release: () => {
transactionSubscriptions.delete(id);
},
};
} else {
// Node-specific transaction subscriptions:
const {nodeTransactionSubscriptions} = storeRef.current.getState();
if (!nodeTransactionSubscriptions.has(key)) {
nodeTransactionSubscriptions.set(key, new Map());
}
const id = nextID++;
nullthrows(nodeTransactionSubscriptions.get(key)).set(id, callback);
return {
release: () => {
const subs: ?Map<number, (_0: Store) => void> =
nodeTransactionSubscriptions.get(key);
if (subs) {
subs.delete(id);
if (subs.size === 0) {
nodeTransactionSubscriptions.delete(key);
}
}
},
};
}
};
const addTransactionMetadata = (metadata: {...}) => {
startNextTreeIfNeeded(storeRef.current);
for (const k of Object.keys(metadata)) {
nullthrows(storeRef.current.getState().nextTree).transactionMetadata[k] =
metadata[k];
}
};
const replaceState = (replacer: TreeState => TreeState) => {
startNextTreeIfNeeded(storeRef.current);
// Use replacer to get the next state:
const nextTree = nullthrows(storeStateRef.current.nextTree);
let replaced;
try {
stateReplacerIsBeingExecuted = true;
replaced = replacer(nextTree);
} finally {
stateReplacerIsBeingExecuted = false;
}
if (replaced === nextTree) {
return;
}
if (__DEV__) {
if (typeof window !== 'undefined') {
window.$recoilDebugStates.push(replaced); // TODO this shouldn't happen here because it's not batched
}
}
// Save changes to nextTree and schedule a React update:
storeStateRef.current.nextTree = replaced;
if (reactMode().early) {
notifyComponents(storeRef.current, storeStateRef.current, replaced);
}
nullthrows(notifyBatcherOfChange.current)();
};
const notifyBatcherOfChange = useRef<null | (mixed => void)>(null);
const setNotifyBatcherOfChange = useCallback(
(x: mixed => void) => {
notifyBatcherOfChange.current = x;
},
[notifyBatcherOfChange],
);
const storeRef: {current: Store} = useRefInitOnce(
() =>
storeProp ?? {
storeID: getNextStoreID(),
getState: () => storeStateRef.current,
replaceState,
getGraph,
subscribeToTransactions,
addTransactionMetadata,
},
);
if (storeProp != null) {
storeRef.current = storeProp;
}
storeStateRef = useRefInitOnce(() =>
initializeState_DEPRECATED != null
? initialStoreState_DEPRECATED(
storeRef.current,
initializeState_DEPRECATED,
)
: initializeState != null
? initialStoreState(initializeState)
: makeEmptyStoreState(),
);
const mutableSource = useMemo(
() =>
createMutableSource?.(
storeStateRef,
() => storeStateRef.current.currentTree.version,
),
[storeStateRef],
);
// Cleanup when the <RecoilRoot> is unmounted
useEffect(() => {
// React is free to call effect cleanup handlers and effects at will, the
// deps array is only an optimization. For example, React strict mode
// will execute each effect twice for testing. Therefore, we need symmetry
// to re-initialize all known atoms after they were cleaned up.
const store = storeRef.current;
for (const atomKey of new Set(store.getState().knownAtoms)) {
initializeNode(store, atomKey, 'get');
}
return () => {
for (const atomKey of store.getState().knownAtoms) {
cleanUpNode(store, atomKey);
}
};
}, [storeRef]);
return (
<AppContext.Provider value={storeRef}>
<MutableSourceContext.Provider value={mutableSource}>
<Batcher setNotifyBatcherOfChange={setNotifyBatcherOfChange} />
{children}
</MutableSourceContext.Provider>
</AppContext.Provider>
);
}
type Props =
| {
initializeState_DEPRECATED?: ({
set: <T>(RecoilValue<T>, T) => void,
setUnvalidatedAtomValues: (Map<string, mixed>) => void,
}) => void,
initializeState?: MutableSnapshot => void,
store_INTERNAL?: Store,
override?: true,
children: React.Node,
}
| {
store_INTERNAL?: Store,
/**
* Defaults to true. If override is true, this RecoilRoot will create a
* new Recoil scope. If override is false and this RecoilRoot is nested
* within another RecoilRoot, this RecoilRoot will perform no function.
* Children of this RecoilRoot will access the Recoil values of the
* nearest ancestor RecoilRoot.
*/
override: false,
children: React.Node,
};
function RecoilRoot(props: Props): React.Node {
const {override, ...propsExceptOverride} = props;
const ancestorStoreRef = useStoreRef();
if (override === false && ancestorStoreRef.current !== defaultStore) {
// If ancestorStoreRef.current !== defaultStore, it means that this
// RecoilRoot is not nested within another.
return props.children;
}
return <RecoilRoot_INTERNAL {...propsExceptOverride} />;
}
function useRecoilStoreID(): StoreID {
return useStoreRef().current.storeID;
}
module.exports = {
RecoilRoot,
useStoreRef,
useRecoilMutableSource,
useRecoilStoreID,
notifyComponents_FOR_TESTING: notifyComponents,
sendEndOfBatchNotifications_FOR_TESTING: sendEndOfBatchNotifications,
};

View File

@@ -0,0 +1,41 @@
/**
* 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 strict
* @format
* @oncall recoil
*/
'use strict';
import type {NodeKey} from './Recoil_State';
// eslint-disable-next-line no-unused-vars
class AbstractRecoilValue<+T> {
key: NodeKey;
constructor(newKey: NodeKey) {
this.key = newKey;
}
toJSON(): {key: string} {
return {key: this.key};
}
}
class RecoilState<T> extends AbstractRecoilValue<T> {}
class RecoilValueReadOnly<+T> extends AbstractRecoilValue<T> {}
export type RecoilValue<T> = RecoilValueReadOnly<T> | RecoilState<T>;
function isRecoilValue(x: mixed): boolean %checks {
return x instanceof RecoilState || x instanceof RecoilValueReadOnly;
}
module.exports = {
AbstractRecoilValue,
RecoilState,
RecoilValueReadOnly,
isRecoilValue,
};

View File

@@ -0,0 +1,378 @@
/**
* 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 strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Loadable} from '../adt/Recoil_Loadable';
import type {ValueOrUpdater} from '../recoil_values/Recoil_callbackTypes';
import type {
AtomValues,
AtomWrites,
NodeKey,
Store,
TreeState,
} from './Recoil_State';
const {
getDownstreamNodes,
getNodeLoadable,
setNodeValue,
} = require('./Recoil_FunctionalCore');
const {getNextComponentID} = require('./Recoil_Keys');
const {getNode, getNodeMaybe} = require('./Recoil_Node');
const {DefaultValue} = require('./Recoil_Node');
const {reactMode} = require('./Recoil_ReactMode');
const {
AbstractRecoilValue,
RecoilState,
RecoilValueReadOnly,
isRecoilValue,
} = require('./Recoil_RecoilValue');
const {invalidateMemoizedSnapshot} = require('./Recoil_SnapshotCache');
const err = require('recoil-shared/util/Recoil_err');
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
function getRecoilValueAsLoadable<T>(
store: Store,
{key}: AbstractRecoilValue<T>,
treeState: TreeState = store.getState().currentTree,
): Loadable<T> {
// Reading from an older tree can cause bugs because the dependencies that we
// discover during the read are lost.
const storeState = store.getState();
if (
!(
treeState.version === storeState.currentTree.version ||
treeState.version === storeState.nextTree?.version ||
treeState.version === storeState.previousTree?.version
)
) {
recoverableViolation('Tried to read from a discarded tree', 'recoil');
}
const loadable = getNodeLoadable<T>(store, treeState, key);
if (loadable.state === 'loading') {
loadable.contents.catch(() => {
/**
* HACK: intercept thrown error here to prevent an uncaught promise exception. Ideally this would happen closer to selector
* execution (perhaps introducing a new ERROR class to be resolved by async selectors that are in an error state)
*/
return;
});
}
return loadable;
}
function applyAtomValueWrites(
atomValues: AtomValues,
writes: AtomWrites,
): AtomValues {
const result = atomValues.clone();
writes.forEach((v, k) => {
if (v.state === 'hasValue' && v.contents instanceof DefaultValue) {
result.delete(k);
} else {
result.set(k, v);
}
});
return result;
}
function valueFromValueOrUpdater<T>(
store: Store,
state: TreeState,
{key}: AbstractRecoilValue<T>,
valueOrUpdater: ValueOrUpdater<T>,
): T | DefaultValue {
if (typeof valueOrUpdater === 'function') {
// Updater form: pass in the current value. Throw if the current value
// is unavailable (namely when updating an async selector that's
// pending or errored):
const current = getNodeLoadable<$FlowFixMe>(store, state, key);
if (current.state === 'loading') {
const msg = `Tried to set atom or selector "${key}" using an updater function while the current state is pending, this is not currently supported.`;
recoverableViolation(msg, 'recoil');
throw err(msg);
} else if (current.state === 'hasError') {
throw current.contents;
}
// T itself may be a function, so our refinement is not sufficient:
return (valueOrUpdater: any)(current.contents); // flowlint-line unclear-type:off
} else {
return valueOrUpdater;
}
}
type Action<T> =
| {
type: 'set',
recoilValue: AbstractRecoilValue<T>,
valueOrUpdater: T | DefaultValue | (T => T | DefaultValue),
}
| {
type: 'setLoadable',
recoilValue: AbstractRecoilValue<T>,
loadable: Loadable<T>,
}
| {
type: 'setUnvalidated',
recoilValue: AbstractRecoilValue<T>,
unvalidatedValue: mixed,
}
| {type: 'markModified', recoilValue: AbstractRecoilValue<T>};
function applyAction(store: Store, state: TreeState, action: Action<mixed>) {
if (action.type === 'set') {
const {recoilValue, valueOrUpdater} = action;
const newValue = valueFromValueOrUpdater(
store,
state,
recoilValue,
valueOrUpdater,
);
const writes = setNodeValue(store, state, recoilValue.key, newValue);
for (const [key, loadable] of writes.entries()) {
writeLoadableToTreeState(state, key, loadable);
}
} else if (action.type === 'setLoadable') {
const {
recoilValue: {key},
loadable,
} = action;
writeLoadableToTreeState(state, key, loadable);
} else if (action.type === 'markModified') {
const {
recoilValue: {key},
} = action;
state.dirtyAtoms.add(key);
} else if (action.type === 'setUnvalidated') {
// Write value directly to state bypassing the Node interface as the node
// definitions may not have been loaded yet when processing the initial snapshot.
const {
recoilValue: {key},
unvalidatedValue,
} = action;
const node = getNodeMaybe(key);
node?.invalidate?.(state);
state.atomValues.delete(key);
state.nonvalidatedAtoms.set(key, unvalidatedValue);
state.dirtyAtoms.add(key);
} else {
recoverableViolation(`Unknown action ${action.type}`, 'recoil');
}
}
function writeLoadableToTreeState(
state: TreeState,
key: NodeKey,
loadable: Loadable<mixed>,
): void {
if (
loadable.state === 'hasValue' &&
loadable.contents instanceof DefaultValue
) {
state.atomValues.delete(key);
} else {
state.atomValues.set(key, loadable);
}
state.dirtyAtoms.add(key);
state.nonvalidatedAtoms.delete(key);
}
function applyActionsToStore(store: Store, actions: Array<Action<mixed>>) {
store.replaceState(state => {
const newState = copyTreeState(state);
for (const action of actions) {
applyAction(store, newState, action);
}
invalidateDownstreams(store, newState);
invalidateMemoizedSnapshot();
return newState;
});
}
function queueOrPerformStateUpdate(store: Store, action: Action<mixed>): void {
if (batchStack.length) {
const actionsByStore = batchStack[batchStack.length - 1];
let actions = actionsByStore.get(store);
if (!actions) {
actionsByStore.set(store, (actions = []));
}
actions.push(action);
} else {
applyActionsToStore(store, [action]);
}
}
const batchStack: Array<Map<Store, Array<Action<mixed>>>> = [];
function batchStart(): () => void {
const actionsByStore = new Map<Store, Array<Action<mixed>>>();
batchStack.push(actionsByStore);
return () => {
for (const [store, actions] of actionsByStore) {
applyActionsToStore(store, actions);
}
const popped = batchStack.pop();
if (popped !== actionsByStore) {
recoverableViolation('Incorrect order of batch popping', 'recoil');
}
};
}
function copyTreeState(state: TreeState): TreeState {
return {
...state,
atomValues: state.atomValues.clone(),
nonvalidatedAtoms: state.nonvalidatedAtoms.clone(),
dirtyAtoms: new Set(state.dirtyAtoms),
};
}
function invalidateDownstreams(store: Store, state: TreeState): void {
// Inform any nodes that were changed or downstream of changes so that they
// can clear out any caches as needed due to the update:
const downstreams = getDownstreamNodes(store, state, state.dirtyAtoms);
for (const key of downstreams) {
getNodeMaybe(key)?.invalidate?.(state);
}
}
function setRecoilValue<T>(
store: Store,
recoilValue: AbstractRecoilValue<T>,
valueOrUpdater: T | DefaultValue | (T => T | DefaultValue),
): void {
queueOrPerformStateUpdate(store, {
type: 'set',
recoilValue,
valueOrUpdater,
});
}
function setRecoilValueLoadable<T>(
store: Store,
recoilValue: AbstractRecoilValue<T>,
loadable: DefaultValue | Loadable<T>,
): void {
if (loadable instanceof DefaultValue) {
return setRecoilValue(store, recoilValue, loadable);
}
queueOrPerformStateUpdate(store, {
type: 'setLoadable',
recoilValue,
loadable: (loadable: Loadable<T>),
});
}
function markRecoilValueModified<T>(
store: Store,
recoilValue: AbstractRecoilValue<T>,
): void {
queueOrPerformStateUpdate(store, {
type: 'markModified',
recoilValue,
});
}
function setUnvalidatedRecoilValue<T>(
store: Store,
recoilValue: AbstractRecoilValue<T>,
unvalidatedValue: T,
): void {
queueOrPerformStateUpdate(store, {
type: 'setUnvalidated',
recoilValue,
unvalidatedValue,
});
}
export type ComponentSubscription = {release: () => void};
function subscribeToRecoilValue<T>(
store: Store,
{key}: AbstractRecoilValue<T>,
callback: TreeState => void,
componentDebugName: ?string = null,
): ComponentSubscription {
const subID = getNextComponentID();
const storeState = store.getState();
if (!storeState.nodeToComponentSubscriptions.has(key)) {
storeState.nodeToComponentSubscriptions.set(key, new Map());
}
nullthrows(storeState.nodeToComponentSubscriptions.get(key)).set(subID, [
componentDebugName ?? '<not captured>',
callback,
]);
// Handle the case that, during the same tick that we are subscribing, an atom
// has been updated by some effect handler. Otherwise we will miss the update.
const mode = reactMode();
if (
mode.early &&
(mode.mode === 'LEGACY' || mode.mode === 'MUTABLE_SOURCE')
) {
const nextTree = store.getState().nextTree;
if (nextTree && nextTree.dirtyAtoms.has(key)) {
callback(nextTree);
}
}
return {
release: () => {
const releaseStoreState = store.getState();
const subs = releaseStoreState.nodeToComponentSubscriptions.get(key);
if (subs === undefined || !subs.has(subID)) {
recoverableViolation(
`Subscription missing at release time for atom ${key}. This is a bug in Recoil.`,
'recoil',
);
return;
}
subs.delete(subID);
if (subs.size === 0) {
releaseStoreState.nodeToComponentSubscriptions.delete(key);
}
},
};
}
function refreshRecoilValue<T>(
store: Store,
recoilValue: AbstractRecoilValue<T>,
): void {
const {currentTree} = store.getState();
const node = getNode(recoilValue.key);
node.clearCache?.(store, currentTree);
}
module.exports = {
RecoilValueReadOnly,
AbstractRecoilValue,
RecoilState,
getRecoilValueAsLoadable,
setRecoilValue,
setRecoilValueLoadable,
markRecoilValueModified,
setUnvalidatedRecoilValue,
subscribeToRecoilValue,
isRecoilValue,
applyAtomValueWrites, // TODO Remove export when deprecating initialStoreState_DEPRECATED in RecoilRoot
batchStart,
writeLoadableToTreeState,
invalidateDownstreams,
copyTreeState,
refreshRecoilValue,
};

24
node_modules/recoil/cjs/core/Recoil_RetainedBy.js.flow generated vendored Normal file
View File

@@ -0,0 +1,24 @@
/**
* 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 strict
* @format
* @oncall recoil
*/
'use strict';
import type {RetentionZone} from './Recoil_RetentionZone';
// This is a separate module to prevent an import cycle.
// Options for how an atom can be retained:
export type RetainedBy =
| 'components' // only retained directly by components
| 'recoilRoot' // lives for the lifetime of the root
| RetentionZone // retained whenever this zone or these zones are retained
| Array<RetentionZone>;
module.exports = undefined;

307
node_modules/recoil/cjs/core/Recoil_Retention.js.flow generated vendored Normal file
View File

@@ -0,0 +1,307 @@
/**
* 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 strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {NodeKey} from './Recoil_Keys';
import type {RetainedBy} from './Recoil_RetainedBy';
import type {Retainable, Store, StoreState, TreeState} from './Recoil_State';
const {cleanUpNode} = require('./Recoil_FunctionalCore');
const {deleteNodeConfigIfPossible, getNode} = require('./Recoil_Node');
const {RetentionZone} = require('./Recoil_RetentionZone');
const gkx = require('recoil-shared/util/Recoil_gkx');
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
const someSet = require('recoil-shared/util/Recoil_someSet');
// Components that aren't mounted after suspending for this long will be assumed
// to be discarded and their resources released.
const SUSPENSE_TIMEOUT_MS = 120000;
const emptySet = new Set<NodeKey>();
function releaseRetainablesNowOnCurrentTree(
store: Store,
retainables: Set<Retainable>,
) {
const storeState = store.getState();
const treeState = storeState.currentTree;
if (storeState.nextTree) {
recoverableViolation(
'releaseNodesNowOnCurrentTree should only be called at the end of a batch',
'recoil',
);
return; // leak memory rather than erase something that's about to be used.
}
const nodes = new Set<NodeKey>();
for (const r of retainables) {
if (r instanceof RetentionZone) {
for (const n of nodesRetainedByZone(storeState, r)) {
nodes.add(n);
}
} else {
nodes.add(r);
}
}
const releasableNodes = findReleasableNodes(store, nodes);
for (const node of releasableNodes) {
releaseNode(store, treeState, node);
}
}
function findReleasableNodes(
store: Store,
searchFromNodes: Set<NodeKey>,
): Set<NodeKey> {
const storeState = store.getState();
const treeState = storeState.currentTree;
const graph = store.getGraph(treeState.version);
const releasableNodes: Set<NodeKey> = new Set(); // mutated to collect answer
const nonReleasableNodes: Set<NodeKey> = new Set();
findReleasableNodesInner(searchFromNodes);
return releasableNodes;
function findReleasableNodesInner(searchFromNodes: Set<NodeKey>): void {
const releasableNodesFoundThisIteration = new Set<NodeKey>();
const downstreams = getDownstreamNodesInTopologicalOrder(
store,
treeState,
searchFromNodes,
releasableNodes, // don't descend into these
nonReleasableNodes, // don't descend into these
);
// Find which of the downstream nodes are releasable and which are not:
for (const node of downstreams) {
// Not releasable if configured to be retained forever:
if (getNode(node).retainedBy === 'recoilRoot') {
nonReleasableNodes.add(node);
continue;
}
// Not releasable if retained directly by a component:
if ((storeState.retention.referenceCounts.get(node) ?? 0) > 0) {
nonReleasableNodes.add(node);
continue;
}
// Not releasable if retained by a zone:
if (
zonesThatCouldRetainNode(node).some(z =>
storeState.retention.referenceCounts.get(z),
)
) {
nonReleasableNodes.add(node);
continue;
}
// Not releasable if it has a non-releasable child (which will already be in
// nonReleasableNodes because we are going in topological order):
const nodeChildren = graph.nodeToNodeSubscriptions.get(node);
if (
nodeChildren &&
someSet(nodeChildren, child => nonReleasableNodes.has(child))
) {
nonReleasableNodes.add(node);
continue;
}
releasableNodes.add(node);
releasableNodesFoundThisIteration.add(node);
}
// If we found any releasable nodes, we need to walk UP from those nodes to
// find whether their parents can now be released as well:
const parents = new Set<NodeKey>();
for (const node of releasableNodesFoundThisIteration) {
for (const parent of graph.nodeDeps.get(node) ?? emptySet) {
if (!releasableNodes.has(parent)) {
parents.add(parent);
}
}
}
if (parents.size) {
findReleasableNodesInner(parents);
}
}
}
// Children before parents
function getDownstreamNodesInTopologicalOrder(
store: Store,
treeState: TreeState,
nodes: Set<NodeKey>, // Mutable set is destroyed in place
doNotDescendInto1: Set<NodeKey>,
doNotDescendInto2: Set<NodeKey>,
): Array<NodeKey> {
const graph = store.getGraph(treeState.version);
const answer = [];
const visited = new Set<NodeKey>();
while (nodes.size > 0) {
visit(nullthrows(nodes.values().next().value));
}
return answer;
function visit(node: NodeKey): void {
if (doNotDescendInto1.has(node) || doNotDescendInto2.has(node)) {
nodes.delete(node);
return;
}
if (visited.has(node)) {
return;
}
const children = graph.nodeToNodeSubscriptions.get(node);
if (children) {
for (const child of children) {
visit(child);
}
}
visited.add(node);
nodes.delete(node);
answer.push(node);
}
}
function releaseNode(store: Store, treeState: TreeState, node: NodeKey) {
if (!gkx('recoil_memory_managament_2020')) {
return;
}
// Atom effects, in-closure caches, etc.:
cleanUpNode(store, node);
// Delete from store state:
const storeState = store.getState();
storeState.knownAtoms.delete(node);
storeState.knownSelectors.delete(node);
storeState.nodeTransactionSubscriptions.delete(node);
storeState.retention.referenceCounts.delete(node);
const zones = zonesThatCouldRetainNode(node);
for (const zone of zones) {
storeState.retention.nodesRetainedByZone.get(zone)?.delete(node);
}
// Note that we DO NOT delete from nodeToComponentSubscriptions because this
// already happens when the last component that was retaining the node unmounts,
// and this could happen either before or after that.
// Delete from TreeState and dep graph:
treeState.atomValues.delete(node);
treeState.dirtyAtoms.delete(node);
treeState.nonvalidatedAtoms.delete(node);
const graph = storeState.graphsByVersion.get(treeState.version);
if (graph) {
const deps = graph.nodeDeps.get(node);
if (deps !== undefined) {
graph.nodeDeps.delete(node);
for (const dep of deps) {
graph.nodeToNodeSubscriptions.get(dep)?.delete(node);
}
}
// No need to delete sub's deps as there should be no subs at this point.
// But an invariant would require deleting nodes in topological order.
graph.nodeToNodeSubscriptions.delete(node);
}
// Node config (for family members only as their configs can be recreated, and
// only if they are not retained within any other Stores):
deleteNodeConfigIfPossible(node);
}
function nodesRetainedByZone(
storeState: StoreState,
zone: RetentionZone,
): Set<NodeKey> {
return storeState.retention.nodesRetainedByZone.get(zone) ?? emptySet;
}
function zonesThatCouldRetainNode(node: NodeKey): Array<RetentionZone> {
const retainedBy = getNode(node).retainedBy;
if (
retainedBy === undefined ||
retainedBy === 'components' ||
retainedBy === 'recoilRoot'
) {
return [];
} else if (retainedBy instanceof RetentionZone) {
return [retainedBy];
} else {
return retainedBy; // it's an array of zones
}
}
function scheduleOrPerformPossibleReleaseOfRetainable(
store: Store,
retainable: Retainable,
) {
const state = store.getState();
if (state.nextTree) {
state.retention.retainablesToCheckForRelease.add(retainable);
} else {
releaseRetainablesNowOnCurrentTree(store, new Set([retainable]));
}
}
function updateRetainCount(
store: Store,
retainable: Retainable,
delta: 1 | -1,
): void {
if (!gkx('recoil_memory_managament_2020')) {
return;
}
const map = store.getState().retention.referenceCounts;
const newCount = (map.get(retainable) ?? 0) + delta;
if (newCount === 0) {
updateRetainCountToZero(store, retainable);
} else {
map.set(retainable, newCount);
}
}
function updateRetainCountToZero(store: Store, retainable: Retainable): void {
if (!gkx('recoil_memory_managament_2020')) {
return;
}
const map = store.getState().retention.referenceCounts;
map.delete(retainable);
scheduleOrPerformPossibleReleaseOfRetainable(store, retainable);
}
function releaseScheduledRetainablesNow(store: Store) {
if (!gkx('recoil_memory_managament_2020')) {
return;
}
const state = store.getState();
releaseRetainablesNowOnCurrentTree(
store,
state.retention.retainablesToCheckForRelease,
);
state.retention.retainablesToCheckForRelease.clear();
}
function retainedByOptionWithDefault(r: RetainedBy | void): RetainedBy {
// The default will change from 'recoilRoot' to 'components' in the future.
return r === undefined ? 'recoilRoot' : r;
}
module.exports = {
SUSPENSE_TIMEOUT_MS,
updateRetainCount,
updateRetainCountToZero,
releaseScheduledRetainablesNow,
retainedByOptionWithDefault,
};

View File

@@ -0,0 +1,23 @@
/**
* 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 strict
* @format
* @oncall recoil
*/
'use strict';
class RetentionZone {}
function retentionZone(): RetentionZone {
return new RetentionZone();
}
module.exports = {
RetentionZone,
retentionZone,
};

430
node_modules/recoil/cjs/core/Recoil_Snapshot.js.flow generated vendored Normal file
View File

@@ -0,0 +1,430 @@
/**
* 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 strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Loadable} from '../adt/Recoil_Loadable';
import type {
ResetRecoilState,
SetRecoilState,
ValueOrUpdater,
} from '../recoil_values/Recoil_callbackTypes';
import type {RecoilValueInfo} from './Recoil_FunctionalCore';
import type {Graph} from './Recoil_GraphTypes';
import type {NodeKey, StoreID} from './Recoil_Keys';
import type {RecoilState, RecoilValue} from './Recoil_RecoilValue';
import type {StateID, Store, StoreState, TreeState} from './Recoil_State';
const {batchUpdates} = require('./Recoil_Batching');
const {initializeNode, peekNodeInfo} = require('./Recoil_FunctionalCore');
const {graph} = require('./Recoil_Graph');
const {getNextStoreID} = require('./Recoil_Keys');
const {
DEFAULT_VALUE,
recoilValues,
recoilValuesForKeys,
} = require('./Recoil_Node');
const {
AbstractRecoilValue,
getRecoilValueAsLoadable,
setRecoilValue,
setUnvalidatedRecoilValue,
} = require('./Recoil_RecoilValueInterface');
const {updateRetainCount} = require('./Recoil_Retention');
const {setInvalidateMemoizedSnapshot} = require('./Recoil_SnapshotCache');
const {
getNextTreeStateVersion,
makeEmptyStoreState,
} = require('./Recoil_State');
const concatIterables = require('recoil-shared/util/Recoil_concatIterables');
const {isSSR} = require('recoil-shared/util/Recoil_Environment');
const err = require('recoil-shared/util/Recoil_err');
const filterIterable = require('recoil-shared/util/Recoil_filterIterable');
const gkx = require('recoil-shared/util/Recoil_gkx');
const mapIterable = require('recoil-shared/util/Recoil_mapIterable');
const {
memoizeOneWithArgsHashAndInvalidation,
} = require('recoil-shared/util/Recoil_Memoize');
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
// Opaque at this surface because it's part of the public API from here.
export type SnapshotID = StateID;
const retainWarning = `
Recoil Snapshots only last for the duration of the callback they are provided to. To keep a Snapshot longer, do this:
const release = snapshot.retain();
try {
await doSomethingWithSnapshot(snapshot);
} finally {
release();
}
This is currently a DEV-only warning but will become a thrown exception in the next release of Recoil.
`;
// A "Snapshot" is "read-only" and captures a specific set of values of atoms.
// However, the data-flow-graph and selector values may evolve as selector
// evaluation functions are executed and async selectors resolve.
class Snapshot {
// eslint-disable-next-line fb-www/no-uninitialized-properties
_store: Store;
_refCount: number = 1;
constructor(storeState: StoreState, parentStoreID?: StoreID) {
this._store = {
storeID: getNextStoreID(),
parentStoreID,
getState: () => storeState,
replaceState: replacer => {
// no batching, so nextTree is never active
storeState.currentTree = replacer(storeState.currentTree);
},
getGraph: version => {
const graphs = storeState.graphsByVersion;
if (graphs.has(version)) {
return nullthrows(graphs.get(version));
}
const newGraph = graph();
graphs.set(version, newGraph);
return newGraph;
},
subscribeToTransactions: () => ({release: () => {}}),
addTransactionMetadata: () => {
throw err('Cannot subscribe to Snapshots');
},
};
// Initialize any nodes that are live in the parent store (primarily so that
// this snapshot gets counted towards the node's live stores count).
// TODO Optimize this when cloning snapshots for callbacks
for (const nodeKey of this._store.getState().knownAtoms) {
initializeNode(this._store, nodeKey, 'get');
updateRetainCount(this._store, nodeKey, 1);
}
this.autoRelease_INTERNAL();
}
retain(): () => void {
if (this._refCount <= 0) {
if (__DEV__) {
throw err('Snapshot has already been released.');
} else {
recoverableViolation(
'Attempt to retain() Snapshot that was already released.',
'recoil',
);
}
}
this._refCount++;
let released = false;
return () => {
if (!released) {
released = true;
this._release();
}
};
}
/**
* Release the snapshot on the next tick. This means the snapshot is retained
* during the execution of the current function using it.
*/
autoRelease_INTERNAL(): void {
if (!isSSR) {
// Use timeout of 10 to workaround Firefox issue: https://github.com/facebookexperimental/Recoil/issues/1936
window.setTimeout(() => this._release(), 10);
}
}
_release(): void {
this._refCount--;
if (this._refCount === 0) {
this._store.getState().nodeCleanupFunctions.forEach(cleanup => cleanup());
this._store.getState().nodeCleanupFunctions.clear();
if (!gkx('recoil_memory_managament_2020')) {
return;
}
// Temporarily nerfing this to allow us to find broken call sites without
// actually breaking anybody yet.
// for (const k of this._store.getState().knownAtoms) {
// updateRetainCountToZero(this._store, k);
// }
} else if (this._refCount < 0) {
if (__DEV__) {
recoverableViolation('Snapshot released an extra time.', 'recoil');
}
}
}
isRetained(): boolean {
return this._refCount > 0;
}
checkRefCount_INTERNAL(): void {
if (gkx('recoil_memory_managament_2020') && this._refCount <= 0) {
if (__DEV__) {
recoverableViolation(retainWarning, 'recoil');
}
// What we will ship later:
// throw err(retainWarning);
}
}
getStore_INTERNAL(): Store {
this.checkRefCount_INTERNAL();
return this._store;
}
getID(): SnapshotID {
this.checkRefCount_INTERNAL();
return this._store.getState().currentTree.stateID;
}
getStoreID(): StoreID {
this.checkRefCount_INTERNAL();
return this._store.storeID;
}
// We want to allow the methods to be destructured and used as accessors
/* eslint-disable fb-www/extra-arrow-initializer */
getLoadable: <T>(RecoilValue<T>) => Loadable<T> = <T>(
recoilValue: RecoilValue<T>,
): Loadable<T> => {
this.checkRefCount_INTERNAL();
return getRecoilValueAsLoadable(this._store, recoilValue);
};
getPromise: <T>(RecoilValue<T>) => Promise<T> = <T>(
recoilValue: RecoilValue<T>,
): Promise<T> => {
this.checkRefCount_INTERNAL();
return this.getLoadable(recoilValue).toPromise();
};
getNodes_UNSTABLE: (
{
isModified?: boolean,
isInitialized?: boolean,
} | void,
) => Iterable<RecoilValue<mixed>> = opt => {
this.checkRefCount_INTERNAL();
// TODO Deal with modified selectors
if (opt?.isModified === true) {
if (opt?.isInitialized === false) {
return [];
}
const state = this._store.getState().currentTree;
return recoilValuesForKeys(state.dirtyAtoms);
}
const knownAtoms = this._store.getState().knownAtoms;
const knownSelectors = this._store.getState().knownSelectors;
return opt?.isInitialized == null
? recoilValues.values()
: opt.isInitialized === true
? recoilValuesForKeys(concatIterables([knownAtoms, knownSelectors]))
: filterIterable(
recoilValues.values(),
({key}) => !knownAtoms.has(key) && !knownSelectors.has(key),
);
};
// Report the current status of a node.
// This peeks the current state and does not affect the snapshot state at all
getInfo_UNSTABLE: <T>(RecoilValue<T>) => RecoilValueInfo<T> = <T>({
key,
}: RecoilValue<T>): RecoilValueInfo<T> => {
this.checkRefCount_INTERNAL();
return peekNodeInfo(this._store, this._store.getState().currentTree, key);
};
map: ((MutableSnapshot) => void) => Snapshot = mapper => {
this.checkRefCount_INTERNAL();
const mutableSnapshot = new MutableSnapshot(this, batchUpdates);
mapper(mutableSnapshot); // if removing batchUpdates from `set` add it here
return mutableSnapshot;
};
asyncMap: ((MutableSnapshot) => Promise<void>) => Promise<Snapshot> =
async mapper => {
this.checkRefCount_INTERNAL();
const mutableSnapshot = new MutableSnapshot(this, batchUpdates);
mutableSnapshot.retain(); // Retain new snapshot during async mapper
await mapper(mutableSnapshot);
// Continue to retain the new snapshot for the user, but auto-release it
// after the next tick, the same as a new synchronous snapshot.
mutableSnapshot.autoRelease_INTERNAL();
return mutableSnapshot;
};
/* eslint-enable fb-www/extra-arrow-initializer */
}
function cloneStoreState(
store: Store,
treeState: TreeState,
bumpVersion: boolean = false,
): StoreState {
const storeState = store.getState();
const version = bumpVersion ? getNextTreeStateVersion() : treeState.version;
return {
// Always clone the TreeState to isolate stores from accidental mutations.
// For example, reading a selector from a cloned snapshot shouldn't cache
// in the original treestate which may cause the original to skip
// initialization of upstream atoms.
currentTree: {
// TODO snapshots shouldn't really have versions because a new version number
// is always assigned when the snapshot is gone to.
version: bumpVersion ? version : treeState.version,
stateID: bumpVersion ? version : treeState.stateID,
transactionMetadata: {...treeState.transactionMetadata},
dirtyAtoms: new Set(treeState.dirtyAtoms),
atomValues: treeState.atomValues.clone(),
nonvalidatedAtoms: treeState.nonvalidatedAtoms.clone(),
},
commitDepth: 0,
nextTree: null,
previousTree: null,
knownAtoms: new Set(storeState.knownAtoms), // FIXME here's a copy
knownSelectors: new Set(storeState.knownSelectors), // FIXME here's a copy
transactionSubscriptions: new Map(),
nodeTransactionSubscriptions: new Map(),
nodeToComponentSubscriptions: new Map(),
queuedComponentCallbacks_DEPRECATED: [],
suspendedComponentResolvers: new Set(),
graphsByVersion: new Map<StateID, Graph>().set(
version,
store.getGraph(treeState.version),
),
retention: {
referenceCounts: new Map(),
nodesRetainedByZone: new Map(),
retainablesToCheckForRelease: new Set(),
},
// FIXME here's a copy
// Create blank cleanup handlers for atoms so snapshots don't re-run
// atom effects.
nodeCleanupFunctions: new Map(
mapIterable(storeState.nodeCleanupFunctions.entries(), ([key]) => [
key,
() => {},
]),
),
};
}
// Factory to build a fresh snapshot
function freshSnapshot(initializeState?: MutableSnapshot => void): Snapshot {
const snapshot = new Snapshot(makeEmptyStoreState());
return initializeState != null ? snapshot.map(initializeState) : snapshot;
}
// Factory to clone a snapshot state
const [memoizedCloneSnapshot, invalidateMemoizedSnapshot] =
memoizeOneWithArgsHashAndInvalidation(
// $FlowFixMe[missing-local-annot]
(store, version) => {
const storeState = store.getState();
const treeState =
version === 'latest'
? storeState.nextTree ?? storeState.currentTree
: nullthrows(storeState.previousTree);
return new Snapshot(cloneStoreState(store, treeState), store.storeID);
},
(store, version) =>
String(version) +
String(store.storeID) +
String(store.getState().nextTree?.version) +
String(store.getState().currentTree.version) +
String(store.getState().previousTree?.version),
);
// Avoid circular dependencies
setInvalidateMemoizedSnapshot(invalidateMemoizedSnapshot);
function cloneSnapshot(
store: Store,
version: 'latest' | 'previous' = 'latest',
): Snapshot {
const snapshot = memoizedCloneSnapshot(store, version);
if (!snapshot.isRetained()) {
invalidateMemoizedSnapshot();
return memoizedCloneSnapshot(store, version);
}
return snapshot;
}
class MutableSnapshot extends Snapshot {
_batch: (() => void) => void;
constructor(snapshot: Snapshot, batch: (() => void) => void) {
super(
cloneStoreState(
snapshot.getStore_INTERNAL(),
snapshot.getStore_INTERNAL().getState().currentTree,
true,
),
snapshot.getStoreID(),
);
this._batch = batch;
}
set: SetRecoilState = <T>(
recoilState: RecoilState<T>,
newValueOrUpdater: ValueOrUpdater<T>,
) => {
this.checkRefCount_INTERNAL();
const store = this.getStore_INTERNAL();
// This batchUpdates ensures this `set` is applied immediately and you can
// read the written value after calling `set`. I would like to remove this
// behavior and only batch in `Snapshot.map`, but this would be a breaking
// change potentially.
this._batch(() => {
updateRetainCount(store, recoilState.key, 1);
setRecoilValue(this.getStore_INTERNAL(), recoilState, newValueOrUpdater);
});
};
reset: ResetRecoilState = <T>(recoilState: RecoilState<T>) => {
this.checkRefCount_INTERNAL();
const store = this.getStore_INTERNAL();
// See note at `set` about batched updates.
this._batch(() => {
updateRetainCount(store, recoilState.key, 1);
setRecoilValue(this.getStore_INTERNAL(), recoilState, DEFAULT_VALUE);
});
};
setUnvalidatedAtomValues_DEPRECATED: (Map<NodeKey, mixed>) => void = (
values: Map<NodeKey, mixed>,
) => {
this.checkRefCount_INTERNAL();
const store = this.getStore_INTERNAL();
// See note at `set` about batched updates.
batchUpdates(() => {
for (const [k, v] of values.entries()) {
updateRetainCount(store, k, 1);
setUnvalidatedRecoilValue(store, new AbstractRecoilValue(k), v);
}
});
};
}
module.exports = {
Snapshot,
MutableSnapshot,
freshSnapshot,
cloneSnapshot,
};

View File

@@ -0,0 +1,26 @@
/**
* 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 strict-local
* @format
* @oncall recoil
*/
'use strict';
let _invalidateMemoizedSnapshot: ?() => void = null;
function setInvalidateMemoizedSnapshot(invalidate: () => void): void {
_invalidateMemoizedSnapshot = invalidate;
}
function invalidateMemoizedSnapshot(): void {
_invalidateMemoizedSnapshot?.();
}
module.exports = {
setInvalidateMemoizedSnapshot,
invalidateMemoizedSnapshot,
};

184
node_modules/recoil/cjs/core/Recoil_State.js.flow generated vendored Normal file
View File

@@ -0,0 +1,184 @@
/**
* 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 strict
* @format
* @oncall recoil
*/
'use strict';
import type {Loadable} from '../adt/Recoil_Loadable';
import type {PersistentMap} from '../adt/Recoil_PersistentMap';
import type {Graph} from './Recoil_GraphTypes';
import type {ComponentID, NodeKey, StateID, StoreID} from './Recoil_Keys';
import type {RetentionZone} from './Recoil_RetentionZone';
const {persistentMap} = require('../adt/Recoil_PersistentMap');
const {graph} = require('./Recoil_Graph');
const {getNextTreeStateVersion} = require('./Recoil_Keys');
export type {ComponentID, NodeKey, StateID, StoreID} from './Recoil_Keys';
// flowlint-next-line unclear-type:off
export type AtomValues = PersistentMap<NodeKey, Loadable<any>>;
// flowlint-next-line unclear-type:off
export type AtomWrites = Map<NodeKey, Loadable<any>>;
type ComponentCallback = TreeState => void;
export type Retainable = RetentionZone | NodeKey;
// TreeState represents the state of a rendered React tree. As such, multiple
// TreeStates may be in play at one time due to concurrent rendering, and each
// TreeState is immutable.
export type TreeState = $ReadOnly<{
// Version always increments when moving from one state to another, even
// if the same state has been seen before.
version: StateID,
// State ID usually increments, but when going to a snapshot that was
// previously rendered the state ID will be re-used:
stateID: StateID,
transactionMetadata: {...},
// Atoms:
dirtyAtoms: Set<NodeKey>,
atomValues: AtomValues,
nonvalidatedAtoms: PersistentMap<NodeKey, mixed>,
}>;
// StoreState represents the state of a Recoil context. It is global and mutable.
// It is updated only during effects, except that the nextTree property is updated
// when atom values change and async requests resolve, and suspendedComponentResolvers
// is updated when components are suspended.
export type StoreState = {
// The "current" TreeState being either directly read from (legacy) or passed
// to useMutableSource (when in use). It is replaced with nextTree when
// a transaction is completed or async request finishes:
currentTree: TreeState,
// The TreeState that is written to when during the course of a transaction
// (generally equal to a React batch) when atom values are updated.
nextTree: null | TreeState,
// This TreeState exists only during the time that components and observers
// are being notified of a newly-committed tree:
previousTree: null | TreeState,
// Incremented when finishing a batch; used to detect cascading updates.
commitDepth: number,
// Node lifetimes
knownAtoms: Set<NodeKey>,
knownSelectors: Set<NodeKey>,
+retention: {
referenceCounts: Map<NodeKey | RetentionZone, number>,
nodesRetainedByZone: Map<RetentionZone, Set<NodeKey>>,
retainablesToCheckForRelease: Set<Retainable>,
},
// Between the time a component is first used and when it is released,
// there will be a function in this map that cleans up the node upon release
// (or upon root unmount).
+nodeCleanupFunctions: Map<NodeKey, () => void>,
// Which components depend on a specific node. (COMMIT/SUSPEND updates).
+nodeToComponentSubscriptions: Map<
NodeKey,
Map<ComponentID, [string, ComponentCallback]>,
>,
// Which nodes depend on which. A pure function of the version (atom state)
// and nodeToComponentSubscriptions. Recomputed when:
// (1) A transaction occurs (atoms written) or
// (2) An async request is completed or
// (3) (IN FUTURE) nodeToComponentSubscriptions is updated
// How incremental computation is performed:
// In case of transactions, we walk downward from the updated atoms
// In case of async request completion, we walk downward from updated selector
// In (future) case of component subscriptions updated, we walk upwards from
// component and then downward from any no-longer-depended on nodes
+graphsByVersion: Map<StateID, Graph>,
// Side note: it would be useful to consider async request completion as
// another type of transaction since it should increase version etc. and many
// things have to happen in both of these cases.
// For observing transactions:
+transactionSubscriptions: Map<number, (Store) => void>,
+nodeTransactionSubscriptions: Map<NodeKey, Map<number, (Store) => void>>,
// Callbacks to render external components that are subscribed to nodes
// These are executed at the end of the transaction or asynchronously.
// FIXME remove when removing useInterface
+queuedComponentCallbacks_DEPRECATED: Array<ComponentCallback>,
// Promise resolvers to wake any components we suspended with React Suspense
+suspendedComponentResolvers: Set<() => void>,
};
// The Store is just the interface that is made available via the context.
// It is constant within a given Recoil root.
export type Store = $ReadOnly<{
storeID: StoreID,
parentStoreID?: StoreID,
getState: () => StoreState,
replaceState: ((TreeState) => TreeState) => void,
getGraph: StateID => Graph,
subscribeToTransactions: ((Store) => void, ?NodeKey) => {release: () => void},
addTransactionMetadata: ({...}) => void,
}>;
export type StoreRef = {
current: Store,
};
function makeEmptyTreeState(): TreeState {
const version = getNextTreeStateVersion();
return {
version,
stateID: version,
transactionMetadata: {},
dirtyAtoms: new Set(),
atomValues: persistentMap(),
nonvalidatedAtoms: persistentMap(),
};
}
function makeEmptyStoreState(): StoreState {
const currentTree = makeEmptyTreeState();
return {
currentTree,
nextTree: null,
previousTree: null,
commitDepth: 0,
knownAtoms: new Set(),
knownSelectors: new Set(),
transactionSubscriptions: new Map(),
nodeTransactionSubscriptions: new Map(),
nodeToComponentSubscriptions: new Map(),
queuedComponentCallbacks_DEPRECATED: [],
suspendedComponentResolvers: new Set(),
graphsByVersion: new Map<StateID, Graph>().set(
currentTree.version,
graph(),
),
retention: {
referenceCounts: new Map(),
nodesRetainedByZone: new Map(),
retainablesToCheckForRelease: new Set(),
},
nodeCleanupFunctions: new Map(),
};
}
module.exports = {
makeEmptyTreeState,
makeEmptyStoreState,
getNextTreeStateVersion,
};

View File

@@ -0,0 +1,476 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Store} from '../Recoil_State';
import type {MutableSnapshot} from 'Recoil_Snapshot';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useState,
act,
useSetRecoilState,
atom,
constSelector,
selector,
asyncSelector,
ReadsAtom,
componentThatReadsAndWritesAtom,
flushPromisesAndTimers,
renderElements,
renderUnwrappedElements,
RecoilRoot,
useStoreRef;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useState} = require('react'));
({act} = require('ReactTestUtils'));
({useSetRecoilState} = require('../../hooks/Recoil_Hooks'));
atom = require('../../recoil_values/Recoil_atom');
constSelector = require('../../recoil_values/Recoil_constSelector');
selector = require('../../recoil_values/Recoil_selector');
({
asyncSelector,
ReadsAtom,
componentThatReadsAndWritesAtom,
flushPromisesAndTimers,
renderElements,
renderUnwrappedElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({RecoilRoot, useStoreRef} = require('../Recoil_RecoilRoot'));
});
describe('initializeState', () => {
testRecoil('initialize atom', () => {
const myAtom = atom({
key: 'RecoilRoot - initializeState - atom',
default: 'DEFAULT',
});
const mySelector = constSelector(myAtom);
function initializeState({set, getLoadable}: MutableSnapshot) {
expect(getLoadable(myAtom).contents).toEqual('DEFAULT');
expect(getLoadable(mySelector).contents).toEqual('DEFAULT');
set(myAtom, 'INITIALIZE');
expect(getLoadable(myAtom).contents).toEqual('INITIALIZE');
expect(getLoadable(mySelector).contents).toEqual('INITIALIZE');
}
const container = renderElements(
<RecoilRoot initializeState={initializeState}>
<ReadsAtom atom={myAtom} />
<ReadsAtom atom={mySelector} />
</RecoilRoot>,
);
expect(container.textContent).toEqual('"INITIALIZE""INITIALIZE"');
});
testRecoil('initialize selector', () => {
const myAtom = atom({
key: 'RecoilRoot - initializeState - selector',
default: 'DEFAULT',
});
// $FlowFixMe[incompatible-call]
const mySelector = selector({
key: 'RecoilRoot - initializeState - selector selector',
get: ({get}) => get(myAtom),
// $FlowFixMe[incompatible-call]
set: ({set}, newValue) => set(myAtom, newValue),
});
function initializeState({set, getLoadable}: MutableSnapshot) {
expect(getLoadable(myAtom).contents).toEqual('DEFAULT');
expect(getLoadable(mySelector).contents).toEqual('DEFAULT');
set(mySelector, 'INITIALIZE');
expect(getLoadable(myAtom).contents).toEqual('INITIALIZE');
expect(getLoadable(mySelector).contents).toEqual('INITIALIZE');
}
const container = renderElements(
<RecoilRoot initializeState={initializeState}>
<ReadsAtom atom={myAtom} />
<ReadsAtom atom={mySelector} />
</RecoilRoot>,
);
expect(container.textContent).toEqual('"INITIALIZE""INITIALIZE"');
});
testRecoil(
'Atom Effects run with global initialization',
async ({strictMode, concurrentMode}) => {
let effectRan = 0;
let effectCleanup = 0;
const myAtom = atom<string>({
key: 'RecoilRoot - initializeState - atom effects',
default: 'DEFAULT',
effects: [
({setSelf}) => {
effectRan++;
setSelf('EFFECT');
return () => {
effectCleanup++;
};
},
],
});
function initializeState({set}: MutableSnapshot) {
set(myAtom, current => {
// Effects are run first
expect(current).toEqual('EFFECT');
return 'INITIALIZE';
});
}
expect(effectRan).toEqual(0);
const container1 = renderElements(
<RecoilRoot initializeState={initializeState}>NO READ</RecoilRoot>,
);
// Effects are run when initialized with initializeState, even if not read.
// Effects are run twice, once before initializeState, then again when rendering.
expect(container1.textContent).toEqual('NO READ');
expect(effectRan).toEqual(strictMode ? (concurrentMode ? 4 : 3) : 2);
// Auto-release of the initializing snapshot
await flushPromisesAndTimers();
expect(effectCleanup).toEqual(strictMode ? (concurrentMode ? 3 : 2) : 1);
// Test again when atom is actually used by the root
effectRan = 0;
effectCleanup = 0;
const container2 = renderElements(
<RecoilRoot initializeState={initializeState}>
<ReadsAtom atom={myAtom} />
</RecoilRoot>,
);
// Effects takes precedence
expect(container2.textContent).toEqual('"EFFECT"');
expect(effectRan).toEqual(strictMode ? (concurrentMode ? 4 : 3) : 2);
await flushPromisesAndTimers();
expect(effectCleanup).toEqual(strictMode ? (concurrentMode ? 3 : 2) : 1);
},
);
testRecoil(
'onSet() called when atom initialized with initializeState',
() => {
const setValues = [];
const myAtom = atom({
key: 'RecoilRoot - initializeState - onSet',
default: 0,
effects: [
({onSet, setSelf}) => {
onSet(value => {
setValues.push(value);
// Confirm setSelf() works when initialized with initializeState
setSelf(value + 1);
});
},
],
});
const [MyAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom);
const c = renderElements(
<RecoilRoot initializeState={({set}) => set(myAtom, 1)}>
<MyAtom />
</RecoilRoot>,
);
expect(c.textContent).toBe('1');
expect(setValues).toEqual([]);
act(() => setAtom(2));
expect(setValues).toEqual([2]);
expect(c.textContent).toBe('3');
},
);
testRecoil(
'Selectors from global initialization are not canceled',
async () => {
const [asyncSel, resolve] = asyncSelector<string, _>();
// $FlowFixMe[incompatible-call]
const depSel = selector({
key: 'RecoilRoot - initializeSTate - async selector',
get: ({get}) => get(asyncSel),
});
const container = renderUnwrappedElements(
<RecoilRoot
// Call initializeState to force a snapshot to be mapped
initializeState={({getLoadable}) => {
getLoadable(asyncSel);
getLoadable(depSel);
}}>
<React.Suspense fallback="loading">
<ReadsAtom atom={asyncSel} />
<ReadsAtom atom={depSel} />
</React.Suspense>
</RecoilRoot>,
);
expect(container.textContent).toEqual('loading');
// Wait for any potential auto-release of initializing snapshot
await flushPromisesAndTimers();
// Ensure that async selectors resolve and are not canceled
act(() => resolve('RESOLVE'));
await flushPromisesAndTimers();
expect(container.textContent).toEqual('"RESOLVE""RESOLVE"');
},
);
testRecoil('initialize with nested store', () => {
const GetStore = ({children}: {children: Store => React.Node}) => {
return children(useStoreRef().current);
};
const container = renderElements(
<RecoilRoot>
<GetStore>
{storeA => (
<RecoilRoot store_INTERNAL={storeA}>
<GetStore>
{storeB => {
expect(storeA === storeB).toBe(true);
return 'NESTED_ROOT/';
}}
</GetStore>
</RecoilRoot>
)}
</GetStore>
ROOT
</RecoilRoot>,
);
expect(container.textContent).toEqual('NESTED_ROOT/ROOT');
});
testRecoil('initializeState is only called once', ({strictMode}) => {
if (strictMode) {
return;
}
const myAtom = atom({
key: 'RecoilRoot/override/atom',
default: 'DEFAULT',
});
const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom);
const initializeState = jest.fn(({set}) => set(myAtom, 'INIT'));
let forceUpdate: () => void = () => {
throw new Error('not rendered');
};
let setRootKey: number => void = _ => {
throw new Error('');
};
function MyRoot() {
const [counter, setCounter] = useState(0);
forceUpdate = () => setCounter(counter + 1);
const [key, setKey] = useState(0);
setRootKey = setKey;
return (
<RecoilRoot key={key} initializeState={initializeState}>
{counter}
<ReadsWritesAtom />
</RecoilRoot>
);
}
const container = renderElements(<MyRoot />);
expect(container.textContent).toEqual('0"INIT"');
act(forceUpdate);
expect(initializeState).toHaveBeenCalledTimes(1);
expect(container.textContent).toEqual('1"INIT"');
act(() => setAtom('SET'));
expect(initializeState).toHaveBeenCalledTimes(1);
expect(container.textContent).toEqual('1"SET"');
act(forceUpdate);
expect(initializeState).toHaveBeenCalledTimes(1);
expect(container.textContent).toEqual('2"SET"');
act(() => setRootKey(1));
expect(initializeState).toHaveBeenCalledTimes(2);
expect(container.textContent).toEqual('2"INIT"');
});
});
testRecoil(
'Impure state updater functions that trigger atom updates are detected',
() => {
// This test ensures that we throw a clean error rather than mysterious breakage
// if the user supplies a state updater function that triggers another update
// within its execution. These state updater functions are supposed to be pure.
// We can't detect all forms of impurity but this one in particular will make
// Recoil break, so we detect it and throw an error.
const atomA = atom({
key: 'RecoilRoot/impureUpdater/a',
default: 0,
});
const atomB = atom({
key: 'RecoilRoot/impureUpdater/b',
default: 0,
});
let update;
function Component() {
const updateA = useSetRecoilState(atomA);
const updateB = useSetRecoilState(atomB);
update = () => {
updateA(() => {
updateB(1);
return 1;
});
};
return null;
}
renderElements(<Component />);
expect(() =>
act(() => {
update();
}),
).toThrow('pure function');
},
);
describe('override prop', () => {
testRecoil(
'RecoilRoots create a new Recoil scope when override is true or undefined',
() => {
const myAtom = atom({
key: 'RecoilRoot/override/atom',
default: 'DEFAULT',
});
const [ReadsWritesAtom, setAtom] =
componentThatReadsAndWritesAtom(myAtom);
const container = renderElements(
<RecoilRoot>
<ReadsAtom atom={myAtom} />
<RecoilRoot>
<ReadsWritesAtom />
</RecoilRoot>
</RecoilRoot>,
);
expect(container.textContent).toEqual('"DEFAULT""DEFAULT"');
act(() => setAtom('SET'));
expect(container.textContent).toEqual('"DEFAULT""SET"');
},
);
testRecoil(
'A RecoilRoot performs no function if override is false and it has an ancestor RecoilRoot',
() => {
const myAtom = atom({
key: 'RecoilRoot/override/atom',
default: 'DEFAULT',
});
const [ReadsWritesAtom, setAtom] =
componentThatReadsAndWritesAtom(myAtom);
const container = renderElements(
<RecoilRoot>
<ReadsAtom atom={myAtom} />
<RecoilRoot override={false}>
<ReadsAtom atom={myAtom} />
<RecoilRoot override={false}>
<ReadsWritesAtom />
</RecoilRoot>
</RecoilRoot>
</RecoilRoot>,
);
expect(container.textContent).toEqual('"DEFAULT""DEFAULT""DEFAULT"');
act(() => setAtom('SET'));
expect(container.textContent).toEqual('"SET""SET""SET"');
},
);
testRecoil(
'Unmounting a nested RecoilRoot with override set to false does not clean up ancestor Recoil atoms',
() => {
const myAtom = atom({
key: 'RecoilRoot/override/atom',
default: 'DEFAULT',
});
const [ReadsWritesAtom, setAtom] =
componentThatReadsAndWritesAtom(myAtom);
let setRenderNestedRoot;
const NestedRootContainer = () => {
const [renderNestedRoot, _setRenderNestedRoot] = useState(true);
setRenderNestedRoot = _setRenderNestedRoot;
return (
renderNestedRoot && (
<RecoilRoot override={false}>
<ReadsWritesAtom />
</RecoilRoot>
)
);
};
const container = renderElements(
<RecoilRoot>
<ReadsAtom atom={myAtom} />
<NestedRootContainer />
</RecoilRoot>,
);
expect(container.textContent).toEqual('"DEFAULT""DEFAULT"');
act(() => setAtom('SET'));
act(() => setRenderNestedRoot(false));
expect(container.textContent).toEqual('"SET"');
},
);
testRecoil(
'A RecoilRoot functions normally if override is false and it does not have an ancestor RecoilRoot',
() => {
const myAtom = atom({
key: 'RecoilRoot/override/atom',
default: 'DEFAULT',
});
const [ReadsWritesAtom, setAtom] =
componentThatReadsAndWritesAtom(myAtom);
const container = renderElements(
<RecoilRoot override={false}>
<ReadsWritesAtom />
</RecoilRoot>,
);
expect(container.textContent).toEqual('"DEFAULT"');
act(() => setAtom('SET'));
expect(container.textContent).toEqual('"SET"');
},
);
});

View File

@@ -0,0 +1,221 @@
/**
* 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 strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let act,
atom,
selector,
getRecoilValueAsLoadable,
setRecoilValue,
setUnvalidatedRecoilValue,
subscribeToRecoilValue,
refreshRecoilValue,
a,
dependsOnAFn,
dependsOnA,
dependsOnDependsOnA,
b,
store;
const testRecoil = getRecoilTestFn(() => {
const {
makeStore,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
({act} = require('ReactTestUtils'));
atom = require('../../recoil_values/Recoil_atom');
selector = require('../../recoil_values/Recoil_selector');
({
getRecoilValueAsLoadable,
setRecoilValue,
setUnvalidatedRecoilValue,
subscribeToRecoilValue,
refreshRecoilValue,
} = require('../Recoil_RecoilValueInterface'));
a = atom<number>({key: 'a', default: 0});
dependsOnAFn = jest.fn(x => x + 1);
dependsOnA = selector({
key: 'dependsOnA',
get: ({get}) => dependsOnAFn(get(a)),
});
dependsOnDependsOnA = selector({
key: 'dependsOnDependsOnA',
// $FlowFixMe[unsafe-addition]
get: ({get}) => get(dependsOnA) + 1,
});
b = atom<number>({
key: 'b',
default: 0,
persistence_UNSTABLE: {
type: 'url',
validator: x => parseInt(x, 10),
},
});
store = makeStore();
});
testRecoil('read default value', () => {
expect(getRecoilValueAsLoadable(store, a)).toMatchObject({
state: 'hasValue',
contents: 0,
});
});
testRecoil('read written value, visited contains written value', () => {
setRecoilValue(store, a, 1);
expect(getRecoilValueAsLoadable(store, a)).toMatchObject({
state: 'hasValue',
contents: 1,
});
});
testRecoil('read selector based on default upstream', () => {
expect(getRecoilValueAsLoadable(store, dependsOnA).contents).toEqual(1);
});
testRecoil('read selector based on written upstream', () => {
setRecoilValue(store, a, 1);
expect(getRecoilValueAsLoadable(store, dependsOnA).contents).toEqual(2);
});
testRecoil('selector subscriber is called when upstream changes', () => {
const callback = jest.fn();
const {release} = subscribeToRecoilValue(store, dependsOnA, callback);
getRecoilValueAsLoadable(store, dependsOnA);
expect(callback).toHaveBeenCalledTimes(0);
setRecoilValue(store, a, 1);
expect(callback).toHaveBeenCalledTimes(1);
release();
setRecoilValue(store, a, 2);
expect(callback).toHaveBeenCalledTimes(1);
});
testRecoil(
'selector is recursively visited when subscribed and upstream changes',
() => {
const callback = jest.fn();
const {release} = subscribeToRecoilValue(
store,
dependsOnDependsOnA,
callback,
);
getRecoilValueAsLoadable(store, dependsOnDependsOnA);
expect(callback).toHaveBeenCalledTimes(0);
setRecoilValue(store, a, 1);
expect(callback).toHaveBeenCalledTimes(1);
release();
setRecoilValue(store, a, 2);
expect(callback).toHaveBeenCalledTimes(1);
},
);
testRecoil('selector function is evaluated only on first read', () => {
dependsOnAFn.mockClear();
const callback = jest.fn();
subscribeToRecoilValue(store, dependsOnA, callback);
getRecoilValueAsLoadable(store, dependsOnA);
expect(dependsOnAFn).toHaveBeenCalledTimes(1); // called once on initial read
act(() => setRecoilValue(store, a, 1337)); // input number must not be used in any other test due to selector-internal caching
getRecoilValueAsLoadable(store, dependsOnA);
expect(dependsOnAFn).toHaveBeenCalledTimes(2); // called again on read following upstream change
getRecoilValueAsLoadable(store, dependsOnA);
expect(dependsOnAFn).toHaveBeenCalledTimes(2); // not called on subsequent read with no upstream change
});
testRecoil('selector cache refresh', () => {
const getA = jest.fn(() => 'A');
// $FlowFixMe[incompatible-call]
const selectorA = selector({
key: 'useRecoilRefresher ancestors A',
get: getA,
});
const getB = jest.fn(({get}) => get(selectorA) + 'B');
const selectorB = selector({
key: 'useRecoilRefresher ancestors B',
get: getB,
});
const getC = jest.fn(({get}) => get(selectorB) + 'C');
const selectorC = selector({
key: 'useRecoilRefresher ancestors C',
get: getC,
});
expect(getRecoilValueAsLoadable(store, selectorC).contents).toEqual('ABC');
expect(getC).toHaveBeenCalledTimes(1);
expect(getB).toHaveBeenCalledTimes(1);
expect(getA).toHaveBeenCalledTimes(1);
expect(getRecoilValueAsLoadable(store, selectorC).contents).toEqual('ABC');
expect(getC).toHaveBeenCalledTimes(1);
expect(getB).toHaveBeenCalledTimes(1);
expect(getA).toHaveBeenCalledTimes(1);
act(() => {
refreshRecoilValue(store, selectorC);
});
expect(getRecoilValueAsLoadable(store, selectorC).contents).toEqual('ABC');
expect(getC).toHaveBeenCalledTimes(2);
expect(getB).toHaveBeenCalledTimes(2);
expect(getA).toHaveBeenCalledTimes(2);
});
testRecoil('atom can go from unvalidated to normal value', () => {
setUnvalidatedRecoilValue(store, b, '1');
expect(getRecoilValueAsLoadable(store, b)).toMatchObject({
state: 'hasValue',
contents: 1,
});
setRecoilValue(store, b, 2);
expect(getRecoilValueAsLoadable(store, b)).toMatchObject({
state: 'hasValue',
contents: 2,
});
});
testRecoil('atom can go from normal to unvalidated value', () => {
setRecoilValue(store, b, 1);
expect(getRecoilValueAsLoadable(store, b)).toMatchObject({
state: 'hasValue',
contents: 1,
});
setUnvalidatedRecoilValue(store, b, '2');
expect(getRecoilValueAsLoadable(store, b)).toMatchObject({
state: 'hasValue',
contents: 2,
});
});
testRecoil('atom can go from unvalidated to unvalidated value', () => {
// Regression test for an issue where setting an unvalidated value when
// already in a has-unvalidated-value state would result in a stale value:
setUnvalidatedRecoilValue(store, b, '1');
expect(getRecoilValueAsLoadable(store, b)).toMatchObject({
state: 'hasValue',
contents: 1,
});
setUnvalidatedRecoilValue(store, b, '2');
expect(getRecoilValueAsLoadable(store, b)).toMatchObject({
state: 'hasValue',
contents: 2,
});
});

View File

@@ -0,0 +1,518 @@
/**
* 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 strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {RecoilState} from '../../core/Recoil_RecoilValue';
import type {RetentionZone} from 'Recoil_RetentionZone';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
act,
atom,
componentThatReadsAndWritesAtom,
gkx,
useRecoilValue,
useRecoilValueLoadable,
useRetain,
useRecoilCallback,
useState,
selector,
renderElements,
retentionZone;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useState} = require('react'));
({act} = require('ReactTestUtils'));
({retentionZone} = require('../../core/Recoil_RetentionZone'));
({
useRecoilValue,
useRecoilValueLoadable,
} = require('../../hooks/Recoil_Hooks'));
({useRecoilCallback} = require('../../hooks/Recoil_useRecoilCallback'));
useRetain = require('../../hooks/Recoil_useRetain');
atom = require('../../recoil_values/Recoil_atom');
selector = require('../../recoil_values/Recoil_selector');
({
componentThatReadsAndWritesAtom,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
gkx = require('recoil-shared/util/Recoil_gkx');
const initialGKValue = gkx('recoil_memory_managament_2020');
gkx.setPass('recoil_memory_managament_2020');
return () => {
initialGKValue || gkx.setFail('recoil_memory_managament_2020');
};
});
let nextKey = 0;
function atomRetainedBy(
retainedBy:
| void
| RetentionZone
| $TEMPORARY$string<'components'>
| $TEMPORARY$array<RetentionZone>,
) {
return atom({
key: `retention/${nextKey++}`,
default: 0,
retainedBy_UNSTABLE: retainedBy,
});
}
function switchComponent(defaultVisible: boolean) {
let innerSetVisible = (_: boolean) => undefined;
const setVisible = (v: boolean) => innerSetVisible(v); // acts like a ref basically
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
function Switch({children}) {
let visible;
[visible, innerSetVisible] = useState(defaultVisible);
return visible ? children : null;
}
return [Switch, setVisible];
}
// Mounts a component that reads the given atom, sets its value, then unmounts it
// and re-mounts it again. Checks whether the value of the atom that was written
// is still observed. If otherChildren is provided, it will be mounted throughout this,
// then at the end it will be unmounted and the atom expected to be released.
function testWhetherAtomIsRetained(
shouldBeRetained: boolean,
node: RecoilState<number>,
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
otherChildren = null,
): void {
const [AtomSwitch, setAtomVisible] = switchComponent(false);
const [OtherChildrenSwitch, setOtherChildrenVisible] = switchComponent(false);
const [ReadsAtomComp, updateAtom] = componentThatReadsAndWritesAtom(node);
const container = renderElements(
<>
<AtomSwitch>
<ReadsAtomComp />
</AtomSwitch>
<OtherChildrenSwitch>{otherChildren}</OtherChildrenSwitch>
</>,
);
expect(container.textContent).toEqual('');
act(() => {
setAtomVisible(true);
setOtherChildrenVisible(true);
});
expect(container.textContent).toEqual('0');
act(() => updateAtom(1));
expect(container.textContent).toEqual('1');
act(() => setAtomVisible(false));
expect(container.textContent).toEqual('');
act(() => setAtomVisible(true));
if (shouldBeRetained) {
expect(container.textContent).toEqual('1');
} else {
expect(container.textContent).toEqual('0');
}
if (otherChildren) {
act(() => {
setAtomVisible(false);
setOtherChildrenVisible(false);
});
expect(container.textContent).toEqual('');
act(() => setAtomVisible(true));
expect(container.textContent).toEqual('0'); // Not expected for root-retained but this doesn't occur in these tests
}
}
describe('Default retention', () => {
testRecoil(
'By default, atoms are retained for the lifetime of the root',
({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
testWhetherAtomIsRetained(true, atomRetainedBy(undefined));
},
);
});
describe('Component-level retention', () => {
testRecoil(
'With retainedBy: components, atoms are released when not in use',
({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
testWhetherAtomIsRetained(false, atomRetainedBy('components'));
},
);
testRecoil(
'An atom is retained by a component being subscribed to it',
({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
const anAtom = atomRetainedBy('components');
function Subscribes() {
useRecoilValue(anAtom);
return null;
}
testWhetherAtomIsRetained(true, anAtom, <Subscribes />);
},
);
testRecoil(
'An atom is retained by a component retaining it explicitly',
({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
const anAtom = atomRetainedBy('components');
function Retains() {
useRetain(anAtom);
return null;
}
testWhetherAtomIsRetained(true, anAtom, <Retains />);
},
);
});
describe('RetentionZone retention', () => {
testRecoil('An atom can be retained via a retention zone', ({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
const zone = retentionZone();
const anAtom = atomRetainedBy(zone);
function RetainsZone() {
useRetain(zone);
return null;
}
testWhetherAtomIsRetained(true, anAtom, <RetainsZone />);
});
});
describe('Retention of and via selectors', () => {
testRecoil(
'An atom is retained when a depending selector is retained',
({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
const anAtom = atomRetainedBy('components');
// $FlowFixMe[incompatible-call]
const aSelector = selector({
key: '...',
retainedBy_UNSTABLE: 'components',
get: ({get}) => {
return get(anAtom);
},
});
function SubscribesToSelector() {
useRecoilValue(aSelector);
return null;
}
testWhetherAtomIsRetained(true, anAtom, <SubscribesToSelector />);
},
);
const flushPromises = async () =>
await act(() => new Promise(window.setImmediate));
testRecoil(
'An async selector is not released when its only subscribed component suspends',
async ({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
let resolve;
let evalCount = 0;
const anAtom = atomRetainedBy('components');
const aSelector = selector({
key: '......',
retainedBy_UNSTABLE: 'components',
get: ({get}) => {
evalCount++;
get(anAtom);
return new Promise(r => {
resolve = r;
});
},
});
function SubscribesToSelector() {
// $FlowFixMe[incompatible-type]
return useRecoilValue(aSelector);
}
// $FlowFixMe[incompatible-type-arg]
const c = renderElements(<SubscribesToSelector />);
expect(c.textContent).toEqual('loading');
expect(evalCount).toBe(1);
// $FlowFixMe[incompatible-call]
act(() => resolve(123));
// We need to let the selector promise resolve but NOT flush timeouts because
// we do release after suspending after a timeout and we don't want that
// to happen because we're testing what happens when it doesn't.
await flushPromises();
await flushPromises();
expect(c.textContent).toEqual('123');
expect(evalCount).toBe(1); // Still in cache, hence wasn't released.
},
);
testRecoil(
'An async selector ignores promises that settle after it is released',
async ({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
let resolve;
let evalCount = 0;
const anAtom = atomRetainedBy('components');
const aSelector = selector({
key: 'retention/asyncSettlesAfterRelease',
retainedBy_UNSTABLE: 'components',
get: ({get}) => {
evalCount++;
get(anAtom);
return new Promise(r => {
resolve = r;
});
},
});
function SubscribesToSelector() {
// Test without using Suspense to avoid complications with Jest promises
// and timeouts when using Suspense. This doesn't affect what's under test.
const l = useRecoilValueLoadable(aSelector);
// $FlowFixMe[incompatible-type]
return l.state === 'loading' ? 'loading' : l.getValue();
}
const [Switch, setMounted] = switchComponent(true);
const c = renderElements(
<Switch>
<SubscribesToSelector />
</Switch>,
);
expect(c.textContent).toEqual('loading');
expect(evalCount).toBe(1);
act(() => setMounted(false)); // release selector while promise is in flight
// $FlowFixMe[incompatible-call]
act(() => resolve(123));
await flushPromises();
act(() => setMounted(true));
expect(evalCount).toBe(2); // selector must be re-evaluated because the resolved value is not in cache
expect(c.textContent).toEqual('loading');
// $FlowFixMe[incompatible-call]
act(() => resolve(123));
await flushPromises();
expect(c.textContent).toEqual('123');
},
);
testRecoil(
'Selector changing deps releases old deps, retains new ones',
({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
const switchAtom = atom({
key: 'switch',
default: false,
});
const depA = atomRetainedBy('components');
const depB = atomRetainedBy('components');
// $FlowFixMe[incompatible-call]
const theSelector = selector({
key: 'sel',
get: ({get}) => {
if (get(switchAtom)) {
return get(depB);
} else {
return get(depA);
}
},
retainedBy_UNSTABLE: 'components',
});
let setup;
function Setup() {
setup = useRecoilCallback(({set}) => () => {
set(depA, 123);
set(depB, 456);
});
return null;
}
function ReadsSelector() {
useRecoilValue(theSelector);
return null;
}
let depAValue;
function ReadsDepA() {
depAValue = useRecoilValue(depA);
return null;
}
let depBValue;
function ReadsDepB() {
depBValue = useRecoilValue(depB);
return null;
}
const [MountSwitch, setAtomsMountedDirectly] = switchComponent(true);
function unmountAndRemount() {
act(() => setAtomsMountedDirectly(false));
act(() => setAtomsMountedDirectly(true));
}
const [ReadsSwitch, setDepSwitch] =
componentThatReadsAndWritesAtom(switchAtom);
renderElements(
<>
<ReadsSelector />
<ReadsSwitch />
<MountSwitch>
<ReadsDepA />
<ReadsDepB />
</MountSwitch>
<Setup />
</>,
);
act(() => {
setup();
});
unmountAndRemount();
expect(depAValue).toBe(123);
expect(depBValue).toBe(0);
act(() => {
setDepSwitch(true);
});
unmountAndRemount();
expect(depAValue).toBe(0);
act(() => {
setup();
});
unmountAndRemount();
expect(depBValue).toBe(456);
},
);
});
describe('Retention during a transaction', () => {
testRecoil(
'Atoms are not released if unmounted and mounted within the same transaction',
({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
const anAtom = atomRetainedBy('components');
const [ReaderA, setAtom] = componentThatReadsAndWritesAtom(anAtom);
const [ReaderB] = componentThatReadsAndWritesAtom(anAtom);
const [SwitchA, setSwitchA] = switchComponent(true);
const [SwitchB, setSwitchB] = switchComponent(false);
const container = renderElements(
<>
<SwitchA>
<ReaderA />
</SwitchA>
<SwitchB>
<ReaderB />
</SwitchB>
</>,
);
act(() => setAtom(123));
act(() => {
setSwitchA(false);
setSwitchB(true);
});
expect(container.textContent).toEqual('123');
},
);
testRecoil(
'An atom is released when two zones retaining it are released at the same time',
({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
const zoneA = retentionZone();
const zoneB = retentionZone();
const anAtom = atomRetainedBy([zoneA, zoneB]);
function RetainsZone({zone}: $TEMPORARY$object<{zone: RetentionZone}>) {
useRetain(zone);
return null;
}
// It's the no-longer-retained-when-unmounting-otherChildren part that is
// important for this test.
testWhetherAtomIsRetained(
true,
anAtom,
<>
<RetainsZone zone={zoneA} />
<RetainsZone zone={zoneB} />
</>,
);
},
);
testRecoil(
'An atom is released when both direct-retainer and zone-retainer are released at the same time',
({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
const zone = retentionZone();
const anAtom = atomRetainedBy(zone);
function RetainsZone() {
useRetain(zone);
return null;
}
function RetainsAtom() {
useRetain(anAtom);
return null;
}
// It's the no-longer-retained-when-unmounting-otherChildren part that is
// important for this test.
testWhetherAtomIsRetained(
true,
anAtom,
<>
<RetainsZone />
<RetainsAtom />
</>,
);
},
);
});

View File

@@ -0,0 +1,687 @@
/**
* 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 strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Snapshot} from '../Recoil_Snapshot';
import type {StateID} from 'Recoil_Keys';
import type {RecoilState, RecoilValueReadOnly} from 'Recoil_RecoilValue';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
act,
useState,
useGotoRecoilSnapshot,
useRecoilTransactionObserver,
atom,
constSelector,
selector,
ReadsAtom,
flushPromisesAndTimers,
asyncSelector,
componentThatReadsAndWritesAtom,
renderElements,
freshSnapshot,
RecoilRoot;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useState} = React);
({act} = require('ReactTestUtils'));
({
useGotoRecoilSnapshot,
useRecoilTransactionObserver,
} = require('../../hooks/Recoil_SnapshotHooks'));
atom = require('../../recoil_values/Recoil_atom');
constSelector = require('../../recoil_values/Recoil_constSelector');
selector = require('../../recoil_values/Recoil_selector');
({
ReadsAtom,
flushPromisesAndTimers,
asyncSelector,
componentThatReadsAndWritesAtom,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({freshSnapshot} = require('../Recoil_Snapshot'));
({RecoilRoot} = require('../Recoil_RecoilRoot'));
});
// Use this to spread proxy results into an object for Jest's toMatchObject()
function getInfo(
snapshot: Snapshot,
node: RecoilState<string> | RecoilValueReadOnly<string>,
) {
return {...snapshot.getInfo_UNSTABLE(node)};
}
// Test first since we are testing all registered nodes
testRecoil('getNodes', () => {
const snapshot = freshSnapshot();
const {getNodes_UNSTABLE} = snapshot;
expect(Array.from(getNodes_UNSTABLE()).length).toEqual(0);
expect(Array.from(getNodes_UNSTABLE({isInitialized: true})).length).toEqual(
0,
);
// expect(Array.from(getNodes_UNSTABLE({isSet: true})).length).toEqual(0);
// Test atoms
const myAtom = atom({key: 'snapshot getNodes atom', default: 'DEFAULT'});
expect(Array.from(getNodes_UNSTABLE()).length).toEqual(1);
expect(Array.from(getNodes_UNSTABLE({isInitialized: true})).length).toEqual(
0,
);
expect(snapshot.getLoadable(myAtom).contents).toEqual('DEFAULT');
const nodesAfterGet = Array.from(getNodes_UNSTABLE());
expect(nodesAfterGet.length).toEqual(1);
expect(nodesAfterGet[0]).toBe(myAtom);
expect(snapshot.getLoadable(nodesAfterGet[0]).contents).toEqual('DEFAULT');
// Test selectors
// $FlowFixMe[incompatible-call]
const mySelector = selector({
key: 'snapshot getNodes selector',
get: ({get}) => get(myAtom) + '-SELECTOR',
});
expect(Array.from(getNodes_UNSTABLE()).length).toEqual(2);
expect(Array.from(getNodes_UNSTABLE({isInitialized: true})).length).toEqual(
1,
);
expect(snapshot.getLoadable(mySelector).contents).toEqual('DEFAULT-SELECTOR');
expect(Array.from(getNodes_UNSTABLE({isInitialized: true})).length).toEqual(
2,
);
// expect(Array.from(getNodes_UNSTABLE({types: ['atom']})).length).toEqual(1);
// const selectorNodes = Array.from(getNodes_UNSTABLE({types: ['selector']}));
// expect(selectorNodes.length).toEqual(1);
// expect(selectorNodes[0]).toBe(mySelector);
// Test dirty atoms
expect(Array.from(getNodes_UNSTABLE()).length).toEqual(2);
// expect(Array.from(getNodes_UNSTABLE({isSet: true})).length).toEqual(0);
expect(
Array.from(snapshot.getNodes_UNSTABLE({isModified: true})).length,
).toEqual(0);
const updatedSnapshot = snapshot.map(({set}) => set(myAtom, 'SET'));
expect(
Array.from(snapshot.getNodes_UNSTABLE({isModified: true})).length,
).toEqual(0);
expect(
Array.from(updatedSnapshot.getNodes_UNSTABLE({isModified: true})).length,
).toEqual(1);
// expect(
// Array.from(snapshot.getNodes_UNSTABLE({isSet: true})).length,
// ).toEqual(0);
// expect(
// Array.from(updatedSnapshot.getNodes_UNSTABLE({isSet: true})).length,
// ).toEqual(1);
const dirtyAtom = Array.from(
updatedSnapshot.getNodes_UNSTABLE({isModified: true}),
)[0];
expect(snapshot.getLoadable(dirtyAtom).contents).toEqual('DEFAULT');
expect(updatedSnapshot.getLoadable(dirtyAtom).contents).toEqual('SET');
// Test reset
const resetSnapshot = updatedSnapshot.map(({reset}) => reset(myAtom));
expect(
Array.from(resetSnapshot.getNodes_UNSTABLE({isModified: true})).length,
).toEqual(1);
// expect(
// Array.from(resetSnapshot.getNodes_UNSTABLE({isSet: true})).length,
// ).toEqual(0);
// TODO Test dirty selectors
});
testRecoil(
'State ID after going to snapshot matches the ID of the snapshot',
() => {
const seenIDs = new Set<StateID>();
const snapshots = [];
let expectedSnapshotID = null;
const myAtom = atom({key: 'Snapshot ID atom', default: 0});
const mySelector = constSelector(myAtom); // For read-only testing below
const transactionObserver = ({
snapshot,
}: {
previousSnapshot: Snapshot,
snapshot: Snapshot,
}) => {
const snapshotID = snapshot.getID();
if (expectedSnapshotID != null) {
expect(seenIDs.has(snapshotID)).toBe(true);
expect(snapshotID).toBe(expectedSnapshotID);
} else {
expect(seenIDs.has(snapshotID)).toBe(false);
}
seenIDs.add(snapshotID);
snapshot.retain();
snapshots.push({snapshotID, snapshot});
};
function TransactionObserver() {
useRecoilTransactionObserver(transactionObserver);
return null;
}
let gotoSnapshot;
function GotoSnapshot() {
gotoSnapshot = useGotoRecoilSnapshot();
return null;
}
const [WriteAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom);
const c = renderElements(
<>
<TransactionObserver />
<GotoSnapshot />
<WriteAtom />
<ReadsAtom atom={mySelector} />
</>,
);
expect(c.textContent).toBe('00');
// Test changing state produces a new state version
act(() => setAtom(1));
act(() => setAtom(2));
expect(snapshots.length).toBe(2);
expect(seenIDs.size).toBe(2);
// Test going to a previous snapshot re-uses the state ID
expectedSnapshotID = snapshots[0].snapshotID;
act(() => gotoSnapshot(snapshots[0].snapshot));
// Test changing state after going to a previous snapshot uses a new version
expectedSnapshotID = null;
act(() => setAtom(3));
// Test mutating a snapshot creates a new version
const transactionSnapshot = snapshots[0].snapshot.map(({set}) => {
set(myAtom, 4);
});
act(() => gotoSnapshot(transactionSnapshot));
expect(seenIDs.size).toBe(4);
expect(snapshots.length).toBe(5);
// Test that added read-only selector doesn't cause an issue getting the
// current version to see the current deps of the selector since we mutated a
// state after going to a snapshot, so that version may not be known by the store.
// If there was a problem, then the component may throw an error when evaluating the selector.
expect(c.textContent).toBe('44');
},
);
testRecoil('Read default loadable from snapshot', () => {
const snapshot: Snapshot = freshSnapshot();
const myAtom = atom({
key: 'Snapshot Atom Default',
default: 'DEFAULT',
});
const atomLoadable = snapshot.getLoadable(myAtom);
expect(atomLoadable.state).toEqual('hasValue');
expect(atomLoadable.contents).toEqual('DEFAULT');
const mySelector = constSelector(myAtom);
const selectorLoadable = snapshot.getLoadable(mySelector);
expect(selectorLoadable.state).toEqual('hasValue');
expect(selectorLoadable.contents).toEqual('DEFAULT');
});
testRecoil('Read async selector from snapshot', async () => {
const snapshot = freshSnapshot();
const otherA = freshSnapshot();
const otherB = freshSnapshot();
const [asyncSel, resolve] = asyncSelector<string, _>();
const nestSel = constSelector(asyncSel);
expect(snapshot.getLoadable(asyncSel).state).toEqual('loading');
expect(snapshot.getLoadable(nestSel).state).toEqual('loading');
expect(otherA.getLoadable(nestSel).state).toEqual('loading');
const otherC = snapshot.map(() => {});
// eslint-disable-next-line jest/valid-expect
const ptest = expect(snapshot.getPromise(asyncSel)).resolves.toEqual(
'SET VALUE',
);
act(() => resolve('SET VALUE'));
await ptest;
await expect(snapshot.getPromise(asyncSel)).resolves.toEqual('SET VALUE');
expect(snapshot.getLoadable(asyncSel).contents).toEqual('SET VALUE');
await expect(snapshot.getPromise(nestSel)).resolves.toEqual('SET VALUE');
await expect(otherA.getPromise(nestSel)).resolves.toEqual('SET VALUE');
await expect(otherB.getPromise(nestSel)).resolves.toEqual('SET VALUE');
await expect(otherC.getPromise(nestSel)).resolves.toEqual('SET VALUE');
});
testRecoil('Sync map of snapshot', () => {
const snapshot = freshSnapshot();
const myAtom = atom({
key: 'Snapshot Map Sync',
default: 'DEFAULT',
});
const mySelector = constSelector(myAtom);
const atomLoadable = snapshot.getLoadable(myAtom);
expect(atomLoadable.state).toEqual('hasValue');
expect(atomLoadable.contents).toEqual('DEFAULT');
const selectorLoadable = snapshot.getLoadable(mySelector);
expect(selectorLoadable.state).toEqual('hasValue');
expect(selectorLoadable.contents).toEqual('DEFAULT');
const setSnapshot = snapshot.map(({set}) => {
set(myAtom, 'SET');
});
const setAtomLoadable = setSnapshot.getLoadable(myAtom);
expect(setAtomLoadable.state).toEqual('hasValue');
expect(setAtomLoadable.contents).toEqual('SET');
const setSelectorLoadable = setSnapshot.getLoadable(myAtom);
expect(setSelectorLoadable.state).toEqual('hasValue');
expect(setSelectorLoadable.contents).toEqual('SET');
const resetSnapshot = snapshot.map(({reset}) => {
reset(myAtom);
});
const resetAtomLoadable = resetSnapshot.getLoadable(myAtom);
expect(resetAtomLoadable.state).toEqual('hasValue');
expect(resetAtomLoadable.contents).toEqual('DEFAULT');
const resetSelectorLoadable = resetSnapshot.getLoadable(myAtom);
expect(resetSelectorLoadable.state).toEqual('hasValue');
expect(resetSelectorLoadable.contents).toEqual('DEFAULT');
});
testRecoil('Async map of snapshot', async () => {
const snapshot = freshSnapshot();
const myAtom = atom({
key: 'Snapshot Map Async',
default: 'DEFAULT',
});
const [beforeAsyncSel, resolveBeforeMap] = asyncSelector<string, _>();
const [duringAsyncSel, resolveDuringMap] = asyncSelector<string, _>();
const [afterAsyncSel, resolveAfterMap] = asyncSelector<string, _>();
const depAsyncSel = selector({
key: 'snapshot asyncMap dep selector',
get: () => afterAsyncSel,
});
resolveBeforeMap('BEFORE');
const newSnapshotPromise = snapshot.asyncMap(async ({getPromise, set}) => {
await expect(getPromise(beforeAsyncSel)).resolves.toBe('BEFORE');
await expect(getPromise(duringAsyncSel)).resolves.toBe('DURING');
// Test that depAsyncSel is first used while mapping the snapshot.
// If the snapshot is auto-released too early the async selector will be
// canceled.
getPromise(depAsyncSel);
// Test that mapped snapshot is not auto-released too early
await flushPromisesAndTimers();
set(myAtom, 'VALUE');
});
resolveDuringMap('DURING');
const newSnapshot = await newSnapshotPromise;
expect(newSnapshot.isRetained()).toBe(true);
resolveAfterMap('AFTER');
await expect(newSnapshot.getPromise(myAtom)).resolves.toBe('VALUE');
await expect(newSnapshot.getPromise(beforeAsyncSel)).resolves.toBe('BEFORE');
await expect(newSnapshot.getPromise(duringAsyncSel)).resolves.toBe('DURING');
await expect(newSnapshot.getPromise(afterAsyncSel)).resolves.toBe('AFTER');
await expect(newSnapshot.getPromise(depAsyncSel)).resolves.toBe('AFTER');
});
testRecoil('getInfo', () => {
const snapshot = freshSnapshot();
const myAtom = atom<string>({
key: 'snapshot getInfo atom',
default: 'DEFAULT',
});
// $FlowFixMe[incompatible-call]
const selectorA = selector({
key: 'getInfo A',
get: ({get}) => get(myAtom),
});
const selectorB = selector({
key: 'getInfo B',
get: ({get}) => get(selectorA) + get(myAtom),
});
// Initial status
expect(getInfo(snapshot, myAtom)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}),
isActive: false,
isSet: false,
isModified: false,
type: 'atom',
});
expect(Array.from(getInfo(snapshot, myAtom).deps)).toEqual([]);
expect(Array.from(getInfo(snapshot, myAtom).subscribers.nodes)).toEqual([]);
expect(getInfo(snapshot, selectorA)).toMatchObject({
loadable: undefined,
isActive: false,
isSet: false,
isModified: false,
type: 'selector',
});
expect(Array.from(getInfo(snapshot, selectorA).deps)).toEqual([]);
expect(Array.from(getInfo(snapshot, selectorA).subscribers.nodes)).toEqual(
[],
);
// $FlowFixMe[incompatible-call]
expect(getInfo(snapshot, selectorB)).toMatchObject({
loadable: undefined,
isActive: false,
isSet: false,
isModified: false,
type: 'selector',
});
// $FlowFixMe[incompatible-call]
expect(Array.from(getInfo(snapshot, selectorB).deps)).toEqual([]);
// $FlowFixMe[incompatible-call]
expect(Array.from(getInfo(snapshot, selectorB).subscribers.nodes)).toEqual(
[],
);
// After reading values
snapshot.getLoadable(selectorB);
expect(getInfo(snapshot, myAtom)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}),
isActive: true,
isSet: false,
isModified: false,
type: 'atom',
});
expect(Array.from(getInfo(snapshot, myAtom).deps)).toEqual([]);
expect(Array.from(getInfo(snapshot, myAtom).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorA, selectorB]),
);
expect(getInfo(snapshot, selectorA)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
expect(Array.from(getInfo(snapshot, selectorA).deps)).toEqual(
expect.arrayContaining([myAtom]),
);
expect(Array.from(getInfo(snapshot, selectorA).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorB]),
);
// $FlowFixMe[incompatible-call]
expect(getInfo(snapshot, selectorB)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULTDEFAULT',
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
// $FlowFixMe[incompatible-call]
expect(Array.from(getInfo(snapshot, selectorB).deps)).toEqual(
expect.arrayContaining([myAtom, selectorA]),
);
// $FlowFixMe[incompatible-call]
expect(Array.from(getInfo(snapshot, selectorB).subscribers.nodes)).toEqual(
[],
);
// After setting a value
const setSnapshot = snapshot.map(({set}) => set(myAtom, 'SET'));
setSnapshot.getLoadable(selectorB); // Read value to prime
expect(getInfo(setSnapshot, myAtom)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'SET'}),
isActive: true,
isSet: true,
isModified: true,
type: 'atom',
});
expect(Array.from(getInfo(setSnapshot, myAtom).deps)).toEqual([]);
expect(Array.from(getInfo(setSnapshot, myAtom).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorA, selectorB]),
);
expect(getInfo(setSnapshot, selectorA)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'SET'}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
expect(Array.from(getInfo(setSnapshot, selectorA).deps)).toEqual(
expect.arrayContaining([myAtom]),
);
expect(Array.from(getInfo(setSnapshot, selectorA).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorB]),
);
// $FlowFixMe[incompatible-call]
expect(getInfo(setSnapshot, selectorB)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'SETSET'}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
// $FlowFixMe[incompatible-call]
expect(Array.from(getInfo(setSnapshot, selectorB).deps)).toEqual(
expect.arrayContaining([myAtom, selectorA]),
);
// $FlowFixMe[incompatible-call]
expect(Array.from(getInfo(setSnapshot, selectorB).subscribers.nodes)).toEqual(
[],
);
// After reseting a value
const resetSnapshot = setSnapshot.map(({reset}) => reset(myAtom));
resetSnapshot.getLoadable(selectorB); // prime snapshot
expect(getInfo(resetSnapshot, myAtom)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}),
isActive: true,
isSet: false,
isModified: true,
type: 'atom',
});
expect(Array.from(getInfo(resetSnapshot, myAtom).deps)).toEqual([]);
expect(Array.from(getInfo(resetSnapshot, myAtom).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorA, selectorB]),
);
expect(getInfo(resetSnapshot, selectorA)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
expect(Array.from(getInfo(resetSnapshot, selectorA).deps)).toEqual(
expect.arrayContaining([myAtom]),
);
expect(
Array.from(getInfo(resetSnapshot, selectorA).subscribers.nodes),
).toEqual(expect.arrayContaining([selectorB]));
// $FlowFixMe[incompatible-call]
expect(getInfo(resetSnapshot, selectorB)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULTDEFAULT',
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
// $FlowFixMe[incompatible-call]
expect(Array.from(getInfo(resetSnapshot, selectorB).deps)).toEqual(
expect.arrayContaining([myAtom, selectorA]),
);
expect(
// $FlowFixMe[incompatible-call]
Array.from(getInfo(resetSnapshot, selectorB).subscribers.nodes),
).toEqual([]);
});
describe('Retention', () => {
testRecoil('auto-release', async () => {
const snapshot = freshSnapshot();
expect(snapshot.isRetained()).toBe(true);
await flushPromisesAndTimers();
expect(snapshot.isRetained()).toBe(false);
const devStatus = window.__DEV__;
window.__DEV__ = true;
expect(() => snapshot.retain()).toThrow('released');
window.__DEV__ = false;
expect(() => snapshot.retain()).not.toThrow('released');
window.__DEV__ = devStatus;
// TODO enable when recoil_memory_managament_2020 is enforced
// expect(() => snapshot.getID()).toThrow('release');
});
testRecoil('retain()', async () => {
const snapshot = freshSnapshot();
expect(snapshot.isRetained()).toBe(true);
const release2 = snapshot.retain();
await flushPromisesAndTimers();
expect(snapshot.isRetained()).toBe(true);
release2();
expect(snapshot.isRetained()).toBe(false);
});
});
describe('Atom effects', () => {
testRecoil('Standalone snapshot', async ({gks}) => {
let effectsRefCount = 0;
const myAtom = atom({
key: 'snapshot effects standalone',
default: 'DEFAULT',
effects: [
({setSelf}) => {
effectsRefCount++;
setSelf('INIT');
return () => {
effectsRefCount--;
};
},
],
});
expect(effectsRefCount).toBe(0);
const fresh = freshSnapshot();
expect(fresh.getLoadable(myAtom).getValue()).toBe('INIT');
expect(effectsRefCount).toBe(1);
// Auto-release snapshot
await flushPromisesAndTimers();
expect(effectsRefCount).toBe(0);
});
testRecoil('RecoilRoot Snapshot', () => {
let effectsRefCount = 0;
const myAtom = atom({
key: 'snapshot effects RecoilRoot',
default: 'DEFAULT',
effects: [
({setSelf}) => {
effectsRefCount++;
setSelf('INIT');
return () => {
effectsRefCount--;
};
},
],
});
let setMount: boolean => void = () => {
throw new Error('Test Error');
};
function Component() {
const [mount, setState] = useState(false);
setMount = setState;
return mount ? (
<RecoilRoot>
<ReadsAtom atom={myAtom} />
</RecoilRoot>
) : (
'UNMOUNTED'
);
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('UNMOUNTED');
expect(effectsRefCount).toBe(0);
act(() => setMount(true));
expect(c.textContent).toBe('"INIT"');
expect(effectsRefCount).toBe(1);
act(() => setMount(false));
expect(c.textContent).toBe('UNMOUNTED');
expect(effectsRefCount).toBe(0);
});
testRecoil('getStoreID()', () => {
const myAtom = atom({
key: 'snapshot effects storeID',
default: 'DEFAULT',
effects: [
({setSelf, storeID}) => {
// $FlowFixMe[incompatible-call]
setSelf(storeID);
},
],
});
const testSnapshot = freshSnapshot();
expect(testSnapshot.getLoadable(myAtom).getValue()).toBe(
testSnapshot.getStoreID(),
);
});
testRecoil('Parent StoreID', () => {
const myAtom = atom({
key: 'snapshot effects parentStoreID',
effects: [
// $FlowFixMe[missing-local-annot]
({storeID, parentStoreID_UNSTABLE, setSelf}) => {
setSelf({storeID, parentStoreID: parentStoreID_UNSTABLE});
},
],
});
const testSnapshot = freshSnapshot();
const mappedSnapshot = testSnapshot.map(() => {});
expect(mappedSnapshot.getLoadable(myAtom).getValue().storeID).toBe(
mappedSnapshot.getStoreID(),
);
expect(mappedSnapshot.getLoadable(myAtom).getValue().parentStoreID).toBe(
testSnapshot.getStoreID(),
);
});
});

View File

@@ -0,0 +1,58 @@
/**
* 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 strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let unstable_batchedUpdates, batchUpdates, getBatcher, setBatcher;
const testRecoil = getRecoilTestFn(() => {
({unstable_batchedUpdates} = require('ReactDOMLegacy_DEPRECATED'));
({batchUpdates, getBatcher, setBatcher} = require('../Recoil_Batching'));
});
/**
* Cleanup function that will reset the batcher back
* to ReactDOM's resetBatcherToDefault.
*
* Call this at the end of a test that calls setBatcher
* to maintain test purity.
*/
const resetBatcherToDefault = () => {
setBatcher(unstable_batchedUpdates);
};
describe('batcher', () => {
testRecoil('default batcher is ReactDOM unstable_batchedUpdates', () => {
expect(getBatcher()).toEqual(unstable_batchedUpdates);
});
testRecoil('setBatcher sets the batcher function', () => {
const batcherFn = jest.fn();
setBatcher(batcherFn);
expect(getBatcher()).toEqual(batcherFn);
resetBatcherToDefault();
});
testRecoil('batchUpdates calls the batcher', () => {
const batcherFn = jest.fn();
setBatcher(batcherFn);
batchUpdates(() => {});
expect(batcherFn).toHaveBeenCalledTimes(1);
resetBatcherToDefault();
});
});

View File

@@ -0,0 +1,45 @@
/**
* 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
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let a, atom, store, nullthrows, getNodeLoadable, setNodeValue;
const testRecoil = getRecoilTestFn(() => {
const {
makeStore,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
atom = require('../../recoil_values/Recoil_atom');
nullthrows = require('recoil-shared/util/Recoil_nullthrows');
({getNodeLoadable, setNodeValue} = require('../Recoil_FunctionalCore'));
a = atom<number>({key: 'a', default: 0}).key;
store = makeStore();
});
testRecoil('read default value', () => {
expect(getNodeLoadable(store, store.getState().currentTree, a)).toMatchObject(
{
state: 'hasValue',
contents: 0,
},
);
});
testRecoil('setNodeValue returns written value when writing atom', () => {
const writes = setNodeValue(store, store.getState().currentTree, a, 1);
expect(nullthrows(writes.get(a)).contents).toBe(1);
});

View File

@@ -0,0 +1,197 @@
/**
* 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 strict-local
* @format
* @oncall obviz
*/
'use strict';
import type {Loadable, RecoilState, RecoilValue} from '../../Recoil_index';
const {atom, selector, selectorFamily} = require('../../Recoil_index');
const {waitForAll} = require('../../recoil_values/Recoil_WaitFor');
const {
getRecoilValueAsLoadable,
setRecoilValue,
} = require('../Recoil_RecoilValueInterface');
const {performance} = require('perf_hooks');
const {makeStore} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
const ITERATIONS = [1]; // Avoid iterating for automated testing
// const ITERATIONS = [100];
// const ITERATIONS = [1000];
// const ITERATIONS = [10, 100, 1000];
// const ITERATIONS = [10, 100, 1000, 10000];
// const ITERATIONS = [10, 100, 1000, 10000, 100000];
function testPerf(
name: string,
fn: ({iterations: number, perf: (() => void) => void}) => void,
) {
test.each(ITERATIONS)(name, iterations => {
store = makeStore();
const perf = (cb: () => void) => {
const BEGIN = performance.now();
cb();
const END = performance.now();
console.log(`${name}(${iterations})`, END - BEGIN);
};
fn({iterations, perf});
});
}
let store = makeStore();
function getNodeLoadable<T>(recoilValue: RecoilValue<T>): Loadable<T> {
return getRecoilValueAsLoadable(store, recoilValue);
}
function getNodeValue<T>(recoilValue: RecoilValue<T>): T {
return getNodeLoadable(recoilValue).getValue();
}
function setNode(recoilValue: RecoilState<string>, value: mixed) {
setRecoilValue(store, recoilValue, value);
// $FlowFixMe[cannot-write]
// $FlowFixMe[unsafe-arithmetic]
store.getState().currentTree.version++;
}
let nextAtomKey = 0;
function createAtoms(num: number): Array<RecoilState<string>> {
const atoms = Array(num);
const atomKey = nextAtomKey++;
for (let i = 0; i < num; i++) {
atoms[i] = atom({
key: `PERF-${atomKey}-${i}`,
default: 'DEFAULT',
});
}
return atoms;
}
const helpersSelector = () =>
// $FlowFixMe[incompatible-call]
selector({
key: `PERF-helpers-${nextAtomKey++}`,
get: ({getCallback}) => ({
getSnapshot: getCallback(
({snapshot}) =>
() =>
snapshot,
),
}),
});
const getHelpers = () => getNodeValue(helpersSelector());
testPerf('create n atoms', ({iterations}) => {
createAtoms(iterations);
});
testPerf('get n atoms', ({iterations, perf}) => {
const atoms = createAtoms(iterations);
perf(() => {
for (const node of atoms) {
getNodeValue(node);
}
});
});
testPerf('set n atoms', ({iterations, perf}) => {
const atoms = createAtoms(iterations);
perf(() => {
for (const node of atoms) {
setNode(node, 'SET');
}
});
});
testPerf('get n selectors', ({iterations, perf}) => {
const atoms = createAtoms(iterations);
const testFamily = selectorFamily({
key: 'PERF-getselectors',
get:
(id: number) =>
// $FlowFixMe[missing-local-annot]
({get}) =>
get(atoms[id]) + get(atoms[0]),
});
perf(() => {
for (let i = 0; i < iterations; i++) {
getNodeValue(testFamily(i));
}
});
});
testPerf('clone n snapshots', ({iterations, perf}) => {
const atoms = createAtoms(iterations);
const {getSnapshot} = getHelpers();
perf(() => {
for (const node of atoms) {
// Set node to avoid hitting cached snapshots
setNode(node, 'SET');
const snapshot = getSnapshot();
expect(getNodeValue(node)).toBe('SET');
expect(snapshot.getLoadable(node).contents).toBe('SET');
}
});
});
testPerf('get 1 selector with n dependencies', ({iterations, perf}) => {
const atoms = createAtoms(iterations);
perf(() => {
getNodeValue(waitForAll(atoms));
});
});
testPerf('get 1 selector with n dependencies n times', ({iterations, perf}) => {
const atoms = createAtoms(iterations);
perf(() => {
for (let i = 0; i < iterations; i++) {
getNodeValue(waitForAll(atoms));
}
});
});
testPerf('get n selectors n times', ({iterations, perf}) => {
const atoms = createAtoms(iterations);
const testFamily = selectorFamily({
key: 'PERF-getselectors',
get:
(id: number) =>
// $FlowFixMe[missing-local-annot]
({get}) =>
get(atoms[id]) + get(atoms[0]),
});
perf(() => {
for (let i = 0; i < iterations; i++) {
for (let j = 0; j < iterations; j++) {
getNodeValue(testFamily(i));
}
}
});
});
testPerf(
'get n selectors with n dependencies n times',
({iterations, perf}) => {
const atoms = createAtoms(iterations);
const testFamily = selectorFamily<_, number>({
key: 'PERF-getselectors',
get: () => () => waitForAll(atoms),
});
perf(() => {
for (let i = 0; i < iterations; i++) {
for (let j = 0; j < iterations; j++) {
getNodeValue(testFamily(i));
}
}
});
},
);

View File

@@ -0,0 +1,69 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {StoreID as StoreIDType} from 'Recoil_Keys';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React, renderElements, RecoilRoot, useRecoilStoreID;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({RecoilRoot, useRecoilStoreID} = require('../Recoil_RecoilRoot'));
});
testRecoil('useRecoilStoreID', () => {
const storeIDs: {[string]: StoreIDType} = {};
function StoreID({
rootKey,
}:
| $TEMPORARY$object<{rootKey: $TEMPORARY$string<'A'>}>
| $TEMPORARY$object<{rootKey: $TEMPORARY$string<'A1'>}>
| $TEMPORARY$object<{rootKey: $TEMPORARY$string<'A2'>}>
| $TEMPORARY$object<{rootKey: $TEMPORARY$string<'B'>}>) {
const storeID = useRecoilStoreID();
storeIDs[rootKey] = storeID;
return null;
}
function MyApp() {
return (
<div>
<RecoilRoot>
<StoreID rootKey="A" />
<RecoilRoot>
<StoreID rootKey="A1" />
</RecoilRoot>
<RecoilRoot override={false}>
<StoreID rootKey="A2" />
</RecoilRoot>
</RecoilRoot>
<RecoilRoot>
<StoreID rootKey="B" />
</RecoilRoot>
</div>
);
}
renderElements(<MyApp />);
expect('A' in storeIDs).toEqual(true);
expect('A1' in storeIDs).toEqual(true);
expect('A2' in storeIDs).toEqual(true);
expect('B' in storeIDs).toEqual(true);
expect(storeIDs.A).not.toEqual(storeIDs.B);
expect(storeIDs.A).not.toEqual(storeIDs.A1);
expect(storeIDs.A).toEqual(storeIDs.A2);
expect(storeIDs.B).not.toEqual(storeIDs.A1);
expect(storeIDs.B).not.toEqual(storeIDs.A2);
});