recoil
Version:
Recoil - A state management library for React
253 lines (218 loc) • 7.04 kB
Flow
/**
* 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 {
GetHandlers,
NodeCacheRoute,
NodeValueGet,
SetHandlers,
TreeCacheBranch,
TreeCacheLeaf,
TreeCacheNode,
} from './Recoil_TreeCacheImplementationType';
const {isFastRefreshEnabled} = require('../core/Recoil_ReactMode');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
export type Options<T> = {
name?: string,
mapNodeValue?: (value: mixed) => mixed,
onHit?: (node: TreeCacheLeaf<T>) => void,
onSet?: (node: TreeCacheLeaf<T>) => void,
};
class ChangedPathError extends Error {}
class TreeCache<T = mixed> {
_name: ?string;
_numLeafs: number;
// $FlowIssue[unclear-type]
_root: TreeCacheNode<any> | null;
_onHit: $NonMaybeType<Options<T>['onHit']>;
_onSet: $NonMaybeType<Options<T>['onSet']>;
_mapNodeValue: $NonMaybeType<Options<T>['mapNodeValue']>;
constructor(options?: Options<T>) {
this._name = options?.name;
this._numLeafs = 0;
this._root = null;
this._onHit = options?.onHit ?? (() => {});
this._onSet = options?.onSet ?? (() => {});
this._mapNodeValue = options?.mapNodeValue ?? (val => val);
}
size(): number {
return this._numLeafs;
}
// $FlowIssue[unclear-type]
root(): TreeCacheNode<any> | null {
return this._root;
}
get(getNodeValue: NodeValueGet, handlers?: GetHandlers<T>): ?T {
return this.getLeafNode(getNodeValue, handlers)?.value;
}
getLeafNode(
getNodeValue: NodeValueGet,
handlers?: GetHandlers<T>,
): ?TreeCacheLeaf<T> {
if (this._root == null) {
return undefined;
}
// Iterate down the tree based on the current node values until we hit a leaf
// $FlowIssue[unclear-type]
let node: ?TreeCacheNode<any> = this._root;
while (node) {
handlers?.onNodeVisit(node);
if (node.type === 'leaf') {
this._onHit(node);
return node;
}
const nodeValue = this._mapNodeValue(getNodeValue(node.nodeKey));
node = node.branches.get(nodeValue);
}
return undefined;
}
set(route: NodeCacheRoute, value: T, handlers?: SetHandlers<T>): void {
const addLeaf = () => {
// First, setup the branch nodes for the route:
// Iterate down the tree to find or add branch nodes following the route
let node: ?TreeCacheBranch<T>;
let branchKey;
for (const [nodeKey, nodeValue] of route) {
// If the previous root was a leaf, while we not have a get(), it means
// the selector has inconsistent values or implementation changed.
const root = this._root;
if (root?.type === 'leaf') {
throw this.invalidCacheError();
}
// node now refers to the next node down in the tree
const parent = node;
// $FlowFixMe[prop-missing]
// $FlowFixMe[incompatible-type]
node = parent ? parent.branches.get(branchKey) : root;
// $FlowFixMe[prop-missing]
// $FlowFixMe[incompatible-type]
node = node ?? {
type: 'branch',
nodeKey,
parent,
branches: new Map(),
branchKey,
};
// If we found an existing node, confirm it has a consistent value
if (node.type !== 'branch' || node.nodeKey !== nodeKey) {
throw this.invalidCacheError();
}
// Add the branch node to the tree
parent?.branches.set(branchKey, node);
handlers?.onNodeVisit?.(node);
// Prepare for next iteration and install root if it is new.
branchKey = this._mapNodeValue(nodeValue);
this._root = this._root ?? node;
}
// Second, setup the leaf node:
// If there is an existing leaf for this route confirm it is consistent
const oldLeaf: ?TreeCacheNode<T> = node
? node?.branches.get(branchKey)
: this._root;
if (
oldLeaf != null &&
(oldLeaf.type !== 'leaf' || oldLeaf.branchKey !== branchKey)
) {
throw this.invalidCacheError();
}
// Create a new or replacement leaf.
const leafNode = {
type: 'leaf',
value,
parent: node,
branchKey,
};
// Install the leaf and call handlers
node?.branches.set(branchKey, leafNode);
this._root = this._root ?? leafNode;
this._numLeafs++;
this._onSet(leafNode);
handlers?.onNodeVisit?.(leafNode);
};
try {
addLeaf();
} catch (error) {
// If the cache was stale or observed inconsistent values, such as with
// Fast Refresh, then clear it and rebuild with the new values.
if (error instanceof ChangedPathError) {
this.clear();
addLeaf();
} else {
throw error;
}
}
}
// Returns true if leaf was actually deleted from the tree
delete(leaf: TreeCacheLeaf<T>): boolean {
const root = this.root();
if (!root) {
return false;
}
if (leaf === root) {
this._root = null;
this._numLeafs = 0;
return true;
}
// Iterate up from the leaf deleteing it from it's parent's branches.
let node = leaf.parent;
let branchKey = leaf.branchKey;
while (node) {
node.branches.delete(branchKey);
// Stop iterating if we hit the root.
if (node === root) {
if (node.branches.size === 0) {
this._root = null;
this._numLeafs = 0;
} else {
this._numLeafs--;
}
return true;
}
// Stop iterating if there are other branches since we don't need to
// remove any more nodes.
if (node.branches.size > 0) {
break;
}
// Iterate up to our parent
branchKey = node?.branchKey;
node = node.parent;
}
// Confirm that the leaf we are deleting is actually attached to our tree
for (; node !== root; node = node.parent) {
if (node == null) {
return false;
}
}
this._numLeafs--;
return true;
}
clear(): void {
this._numLeafs = 0;
this._root = null;
}
invalidCacheError(): ChangedPathError {
const CHANGED_PATH_ERROR_MESSAGE = isFastRefreshEnabled()
? 'Possible Fast Refresh module reload detected. ' +
'This may also be caused by an selector returning inconsistent values. ' +
'Resetting cache.'
: 'Invalid cache values. This happens when selectors do not return ' +
'consistent values for the same input dependency values. That may also ' +
'be caused when using Fast Refresh to change a selector implementation. ' +
'Resetting cache.';
recoverableViolation(
CHANGED_PATH_ERROR_MESSAGE +
(this._name != null ? ` - ${this._name}` : ''),
'recoil',
);
throw new ChangedPathError();
}
}
module.exports = {TreeCache};