diff --git a/src/core/Recoil_FunctionalCore.js b/src/core/Recoil_FunctionalCore.js index aa3b1557d..d6c93cd0f 100644 --- a/src/core/Recoil_FunctionalCore.js +++ b/src/core/Recoil_FunctionalCore.js @@ -43,15 +43,13 @@ function getNodeLoadable( return getNode(key).get(store, state); } -// Peek at the current value loadable for a node. -// NOTE: Only use in contexts where you don't need to update the store with -// new dependencies for the node! +// Peek at the current value loadable for a node without any evaluation or state change function peekNodeLoadable( store: Store, state: TreeState, key: NodeKey, -): Loadable { - return getNodeLoadable(store, state, key)[1]; +): ?Loadable { + return getNode(key).peek(store, state); } // Write value directly to state bypassing the Node interface as the node diff --git a/src/core/Recoil_Node.js b/src/core/Recoil_Node.js index 7779752ca..6ac8f91b2 100644 --- a/src/core/Recoil_Node.js +++ b/src/core/Recoil_Node.js @@ -39,6 +39,9 @@ export type PersistenceInfo = $ReadOnly<{ export type ReadOnlyNodeOptions = $ReadOnly<{ key: NodeKey, + // Returns the current value without evaluating or modifying state + peek: (Store, TreeState) => ?Loadable, + // Returns the discovered deps and the loadable value of the node get: (Store, TreeState) => [DependencyMap, Loadable], diff --git a/src/core/Recoil_RecoilValueInterface.js b/src/core/Recoil_RecoilValueInterface.js index 536fbc739..daf01badb 100644 --- a/src/core/Recoil_RecoilValueInterface.js +++ b/src/core/Recoil_RecoilValueInterface.js @@ -25,7 +25,6 @@ const Tracing = require('../util/Recoil_Tracing'); const unionSets = require('../util/Recoil_unionSets'); const { getNodeLoadable, - peekNodeLoadable, setNodeValue, setUnvalidatedAtomValue, } = require('./Recoil_FunctionalCore'); @@ -74,8 +73,8 @@ function valueFromValueOrUpdater( // pending or errored): const storeState = store.getState(); const state = storeState.nextTree ?? storeState.currentTree; - // NOTE: This will not update state with node subscriptions. - const current = peekNodeLoadable(store, state, key); + // NOTE: This will evaluate node, but not update state with node subscriptions! + const current = getNodeLoadable(store, state, key)[1]; if (current.state === 'loading') { throw new RecoilValueNotReady(key); } else if (current.state === 'hasError') { diff --git a/src/core/Recoil_Snapshot.js b/src/core/Recoil_Snapshot.js index 842f7b1b5..6bac15342 100644 --- a/src/core/Recoil_Snapshot.js +++ b/src/core/Recoil_Snapshot.js @@ -22,7 +22,10 @@ import type {StateID, Store, StoreState, TreeState} from './Recoil_State'; const gkx = require('../util/Recoil_gkx'); const mapIterable = require('../util/Recoil_mapIterable'); const nullthrows = require('../util/Recoil_nullthrows'); -const {getDownstreamNodes} = require('./Recoil_FunctionalCore'); +const { + getDownstreamNodes, + peekNodeLoadable, +} = require('./Recoil_FunctionalCore'); const {graph} = require('./Recoil_Graph'); const {DEFAULT_VALUE, recoilValues} = require('./Recoil_Node'); const { @@ -130,10 +133,13 @@ class Snapshot { recoilValue: RecoilValue, ) => { this.getLoadable(recoilValue); // Evaluate node to ensure deps are up-to-date - const storeState = this._store.getState(); - const deps = storeState.graphsByVersion - .get(storeState.currentTree.version) - ?.nodeDeps.get(recoilValue.key); + const deps = this._store + .getGraph(this._store.getState().currentTree.version) + .nodeDeps.get(recoilValue.key); + // const storeState = this._store.getState(); + // const deps = storeState.graphsByVersion + // .get(storeState.currentTree.version) + // ?.nodeDeps.get(recoilValue.key); return (function*() { for (const key of deps ?? []) { yield nullthrows(recoilValues.get(key)); @@ -169,6 +175,47 @@ class Snapshot { }; }; + // Report the current status of a node. + // This peeks the current state and does not affect the snapshot state at all + // eslint-disable-next-line fb-www/extra-arrow-initializer + getInfo_UNSTABLE: ( + RecoilValue, + ) => { + loadable: ?Loadable, + isActive: boolean, + isSet: boolean, + isModified: boolean, // TODO report modified selectors + type: 'atom' | 'selector' | void, // void until initialized for now + deps: Iterable>, + subscribers: { + nodes: Iterable>, + }, + } = (recoilValue: RecoilValue) => { + const {key} = recoilValue; + const state = this._store.getState().currentTree; + const graph = this._store.getGraph(state.version); + return { + loadable: peekNodeLoadable(this._store, state, key), + isActive: + this._store.getState().knownAtoms.has(key) || + this._store.getState().knownSelectors.has(key), + isSet: state.atomValues.has(key), + isModified: state.dirtyAtoms.has(key), + type: this._store.getState().knownAtoms.has(key) + ? 'atom' + : this._store.getState().knownSelectors.has(key) + ? 'selector' + : undefined, + // Don't use this.getDeps() as it will evaluate the node + deps: graph.nodeDeps.has(key) + ? Array.from(nullthrows(graph.nodeDeps.get(key))).map(key => + nullthrows(recoilValues.get(key)), + ) + : [], + subscribers: this.getSubscribers_UNSTABLE(recoilValue), + }; + }; + // eslint-disable-next-line fb-www/extra-arrow-initializer map: ((MutableSnapshot) => void) => Snapshot = mapper => { const mutableSnapshot = new MutableSnapshot(this); diff --git a/src/core/__tests__/Recoil_Snapshot-test.js b/src/core/__tests__/Recoil_Snapshot-test.js index 142b85c69..9d510a229 100644 --- a/src/core/__tests__/Recoil_Snapshot-test.js +++ b/src/core/__tests__/Recoil_Snapshot-test.js @@ -33,7 +33,9 @@ test('getNodes', () => { const snapshot = freshSnapshot(); const {getNodes_UNSTABLE} = snapshot; expect(Array.from(getNodes_UNSTABLE()).length).toEqual(0); - 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 @@ -339,11 +341,11 @@ describe('getSubscriptions', () => { // No initial subscribers expect(Array.from(snapshot.getSubscribers_UNSTABLE(myAtom).nodes)).toEqual( - expect.arrayContaining([]), + [], ); expect( Array.from(snapshot.getSubscribers_UNSTABLE(selectorC).nodes), - ).toEqual(expect.arrayContaining([])); + ).toEqual([]); // Evaluate selectorC to update all of its upstream node subscriptions snapshot.getLoadable(selectorC); @@ -358,6 +360,186 @@ describe('getSubscriptions', () => { ).toEqual(expect.arrayContaining([selectorC])); expect( Array.from(snapshot.getSubscribers_UNSTABLE(selectorC).nodes), - ).toEqual(expect.arrayContaining([])); + ).toEqual([]); + }); +}); + +test('getInfo', () => { + const snapshot = freshSnapshot(); + + const myAtom = atom({ + key: 'snapshot getInfo atom', + default: 'DEFAULT', + }); + 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(snapshot.getInfo_UNSTABLE(myAtom)).toMatchObject({ + loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}), + isActive: false, + isSet: false, + isModified: false, + type: undefined, + }); + expect(Array.from(snapshot.getInfo_UNSTABLE(myAtom).deps)).toEqual([]); + expect( + Array.from(snapshot.getInfo_UNSTABLE(myAtom).subscribers.nodes), + ).toEqual([]); + expect(snapshot.getInfo_UNSTABLE(selectorA)).toMatchObject({ + loadable: undefined, + isActive: false, + isSet: false, + isModified: false, + type: undefined, + }); + expect(Array.from(snapshot.getInfo_UNSTABLE(selectorA).deps)).toEqual([]); + expect( + Array.from(snapshot.getInfo_UNSTABLE(selectorA).subscribers.nodes), + ).toEqual([]); + expect(snapshot.getInfo_UNSTABLE(selectorB)).toMatchObject({ + loadable: undefined, + isActive: false, + isSet: false, + isModified: false, + type: undefined, + }); + expect(Array.from(snapshot.getInfo_UNSTABLE(selectorB).deps)).toEqual([]); + expect( + Array.from(snapshot.getInfo_UNSTABLE(selectorB).subscribers.nodes), + ).toEqual([]); + + // After reading values + snapshot.getLoadable(selectorB); + expect(snapshot.getInfo_UNSTABLE(myAtom)).toMatchObject({ + loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}), + isActive: true, + isSet: false, + isModified: false, + type: 'atom', + }); + expect(Array.from(snapshot.getInfo_UNSTABLE(myAtom).deps)).toEqual([]); + expect( + Array.from(snapshot.getInfo_UNSTABLE(myAtom).subscribers.nodes), + ).toEqual(expect.arrayContaining([selectorA, selectorB])); + expect(snapshot.getInfo_UNSTABLE(selectorA)).toMatchObject({ + loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}), + isActive: true, + isSet: false, + isModified: false, + type: 'selector', }); + expect(Array.from(snapshot.getInfo_UNSTABLE(selectorA).deps)).toEqual( + expect.arrayContaining([myAtom]), + ); + expect( + Array.from(snapshot.getInfo_UNSTABLE(selectorA).subscribers.nodes), + ).toEqual(expect.arrayContaining([selectorB])); + expect(snapshot.getInfo_UNSTABLE(selectorB)).toMatchObject({ + loadable: expect.objectContaining({ + state: 'hasValue', + contents: 'DEFAULTDEFAULT', + }), + isActive: true, + isSet: false, + isModified: false, + type: 'selector', + }); + expect(Array.from(snapshot.getInfo_UNSTABLE(selectorB).deps)).toEqual( + expect.arrayContaining([myAtom, selectorA]), + ); + expect( + Array.from(snapshot.getInfo_UNSTABLE(selectorB).subscribers.nodes), + ).toEqual([]); + + // After setting a value + const setSnapshot = snapshot.map(({set}) => set(myAtom, 'SET')); + setSnapshot.getLoadable(selectorB); // Read value to prime + expect(setSnapshot.getInfo_UNSTABLE(myAtom)).toMatchObject({ + loadable: expect.objectContaining({state: 'hasValue', contents: 'SET'}), + isActive: true, + isSet: true, + isModified: true, + type: 'atom', + }); + expect(Array.from(setSnapshot.getInfo_UNSTABLE(myAtom).deps)).toEqual([]); + expect( + Array.from(setSnapshot.getInfo_UNSTABLE(myAtom).subscribers.nodes), + ).toEqual(expect.arrayContaining([selectorA, selectorB])); + expect(setSnapshot.getInfo_UNSTABLE(selectorA)).toMatchObject({ + loadable: expect.objectContaining({state: 'hasValue', contents: 'SET'}), + isActive: true, + isSet: false, + isModified: false, + type: 'selector', + }); + expect(Array.from(setSnapshot.getInfo_UNSTABLE(selectorA).deps)).toEqual( + expect.arrayContaining([myAtom]), + ); + expect( + Array.from(setSnapshot.getInfo_UNSTABLE(selectorA).subscribers.nodes), + ).toEqual(expect.arrayContaining([selectorB])); + expect(setSnapshot.getInfo_UNSTABLE(selectorB)).toMatchObject({ + loadable: expect.objectContaining({state: 'hasValue', contents: 'SETSET'}), + isActive: true, + isSet: false, + isModified: false, + type: 'selector', + }); + expect(Array.from(setSnapshot.getInfo_UNSTABLE(selectorB).deps)).toEqual( + expect.arrayContaining([myAtom, selectorA]), + ); + expect( + Array.from(setSnapshot.getInfo_UNSTABLE(selectorB).subscribers.nodes), + ).toEqual([]); + + // After reseting a value + const resetSnapshot = setSnapshot.map(({reset}) => reset(myAtom)); + resetSnapshot.getLoadable(selectorB); // prime snapshot + expect(resetSnapshot.getInfo_UNSTABLE(myAtom)).toMatchObject({ + loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}), + isActive: true, + isSet: false, + isModified: true, + type: 'atom', + }); + expect(Array.from(resetSnapshot.getInfo_UNSTABLE(myAtom).deps)).toEqual([]); + expect( + Array.from(resetSnapshot.getInfo_UNSTABLE(myAtom).subscribers.nodes), + ).toEqual(expect.arrayContaining([selectorA, selectorB])); + expect(resetSnapshot.getInfo_UNSTABLE(selectorA)).toMatchObject({ + loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}), + isActive: true, + isSet: false, + isModified: false, + type: 'selector', + }); + expect(Array.from(resetSnapshot.getInfo_UNSTABLE(selectorA).deps)).toEqual( + expect.arrayContaining([myAtom]), + ); + expect( + Array.from(resetSnapshot.getInfo_UNSTABLE(selectorA).subscribers.nodes), + ).toEqual(expect.arrayContaining([selectorB])); + expect(resetSnapshot.getInfo_UNSTABLE(selectorB)).toMatchObject({ + loadable: expect.objectContaining({ + state: 'hasValue', + contents: 'DEFAULTDEFAULT', + }), + isActive: true, + isSet: false, + isModified: false, + type: 'selector', + }); + expect(Array.from(resetSnapshot.getInfo_UNSTABLE(selectorB).deps)).toEqual( + expect.arrayContaining([myAtom, selectorA]), + ); + expect( + Array.from(resetSnapshot.getInfo_UNSTABLE(selectorB).subscribers.nodes), + ).toEqual([]); }); diff --git a/src/core/__tests__/Recoil_core-test.js b/src/core/__tests__/Recoil_core-test.js index ceb4db3a4..d21cc2c00 100644 --- a/src/core/__tests__/Recoil_core-test.js +++ b/src/core/__tests__/Recoil_core-test.js @@ -13,14 +13,14 @@ const atom = require('../../recoil_values/Recoil_atom'); const {makeStore} = require('../../testing/Recoil_TestingUtils'); const nullthrows = require('../../util/Recoil_nullthrows'); -const {peekNodeLoadable, setNodeValue} = require('../Recoil_FunctionalCore'); +const {getNodeLoadable, setNodeValue} = require('../Recoil_FunctionalCore'); const a = atom({key: 'a', default: 0}).key; test('read default value', () => { const store = makeStore(); expect( - peekNodeLoadable(store, store.getState().currentTree, a), + getNodeLoadable(store, store.getState().currentTree, a)[1], ).toMatchObject({ state: 'hasValue', contents: 0, diff --git a/src/recoil_values/Recoil_atom.js b/src/recoil_values/Recoil_atom.js index ddac6a64c..53f1dff9a 100644 --- a/src/recoil_values/Recoil_atom.js +++ b/src/recoil_values/Recoil_atom.js @@ -280,6 +280,14 @@ function baseAtom(options: BaseAtomOptions): RecoilState { } } + function myPeek(_store, state: TreeState): ?Loadable { + return ( + state.atomValues.get(key) ?? + cachedAnswerForUnvalidatedValue?.[1] ?? + defaultLoadable + ); + } + function myGet(store: Store, state: TreeState): [DependencyMap, Loadable] { initAtom(store, state, 'get'); @@ -289,7 +297,7 @@ function baseAtom(options: BaseAtomOptions): RecoilState { } else if (state.nonvalidatedAtoms.has(key)) { // Atom value is stored but needs validation before use. // We might have already validated it and have a cached validated value: - if (cachedAnswerForUnvalidatedValue !== undefined) { + if (cachedAnswerForUnvalidatedValue != null) { return cachedAnswerForUnvalidatedValue; } if (persistence == null) { @@ -352,6 +360,10 @@ function baseAtom(options: BaseAtomOptions): RecoilState { const node = registerNode({ key, + peek: myPeek, + get: myGet, + set: mySet, + invalidate, dangerouslyAllowMutability: options.dangerouslyAllowMutability, persistence_UNSTABLE: options.persistence_UNSTABLE ? { @@ -359,9 +371,6 @@ function baseAtom(options: BaseAtomOptions): RecoilState { backButton: options.persistence_UNSTABLE.backButton, } : undefined, - get: myGet, - invalidate, - set: mySet, shouldRestoreFromSnapshots: true, }); return node; diff --git a/src/recoil_values/Recoil_selector_NEW.js b/src/recoil_values/Recoil_selector_NEW.js index 3d331904a..d90231a61 100644 --- a/src/recoil_values/Recoil_selector_NEW.js +++ b/src/recoil_values/Recoil_selector_NEW.js @@ -72,6 +72,7 @@ const cacheWithReferenceEquality = require('../caches/Recoil_cacheWithReferenceE const { detectCircularDependencies, getNodeLoadable, + peekNodeLoadable, setNodeValue, } = require('../core/Recoil_FunctionalCore'); const { @@ -746,6 +747,29 @@ function selector( cache = cache.set(key, val); } + function myPeek(state: TreeState): ?Loadable { + // First, get the current deps for this selector + const currentDeps = state.nodeDeps.get(key) ?? emptySet; + const depValues: Map> = new Map( + Array.from(currentDeps) + .sort() + .map(depKey => [depKey, peekNodeLoadable(state, depKey)]), + ); + + const cacheDepValues = new Map(); + for (const [depKey, depValue] of depValues.entries()) { + if (depValue == null) { + return undefined; + } + cacheDepValues.set(depKey, depValue); + } + + // Always cache and evaluate a selector + // It may provide a result even when not all deps are available. + const cacheKey = cacheKeyFromDepValues(cacheDepValues); + return cache.get(cacheKey); + } + function myGet(store: Store, state: TreeState): [TreeState, Loadable] { initSelector(store); // TODO memoize a value if no deps have changed to avoid a cache lookup @@ -802,15 +826,19 @@ function selector( } return registerNode({ key, - options, + peek: myPeek, get: myGet, set: mySet, + dangerouslyAllowMutability: options.dangerouslyAllowMutability, + shouldRestoreFromSnapshots: false, }); } else { return registerNode({ key, - options, + peek: myPeek, get: myGet, + dangerouslyAllowMutability: options.dangerouslyAllowMutability, + shouldRestoreFromSnapshots: false, }); } }*/ diff --git a/src/recoil_values/Recoil_selector_OLD.js b/src/recoil_values/Recoil_selector_OLD.js index bf34577c3..408a3f71d 100644 --- a/src/recoil_values/Recoil_selector_OLD.js +++ b/src/recoil_values/Recoil_selector_OLD.js @@ -72,6 +72,7 @@ const { const cacheWithReferenceEquality = require('../caches/Recoil_cacheWithReferenceEquality'); const { getNodeLoadable, + peekNodeLoadable, setNodeValue, } = require('../core/Recoil_FunctionalCore'); const { @@ -365,6 +366,30 @@ function selector( } } + function myPeek(store: Store, state: TreeState): ?Loadable { + // First, get the current deps for this selector + const currentDeps = + store.getGraph(state.version).nodeDeps.get(key) ?? emptySet; + const depValues: Map> = new Map( + Array.from(currentDeps) + .sort() + .map(depKey => [depKey, peekNodeLoadable(store, state, depKey)]), + ); + + const cacheDepValues = new Map(); + for (const [depKey, depValue] of depValues.entries()) { + if (depValue == null) { + return undefined; + } + cacheDepValues.set(depKey, depValue); + } + + // Always cache and evaluate a selector + // It may provide a result even when not all deps are available. + const cacheKey = cacheKeyFromDepValues(cacheDepValues); + return cache.get(cacheKey); + } + function myGet(store: Store, state: TreeState): [DependencyMap, Loadable] { initSelector(store); // TODO memoize a value if no deps have changed to avoid a cache lookup @@ -432,16 +457,18 @@ function selector( } return registerNode({ key, - dangerouslyAllowMutability: options.dangerouslyAllowMutability, + peek: myPeek, get: myGet, set: mySet, + dangerouslyAllowMutability: options.dangerouslyAllowMutability, shouldRestoreFromSnapshots: false, }); } else { return registerNode({ key, - dangerouslyAllowMutability: options.dangerouslyAllowMutability, + peek: myPeek, get: myGet, + dangerouslyAllowMutability: options.dangerouslyAllowMutability, shouldRestoreFromSnapshots: false, }); }