Skip to content

Commit

Permalink
Snapshot.getInfo() for Dev Tools (facebookexperimental#504)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: facebookexperimental#504

Add a `getInfo_UNSTABLE()` method to `Snapshot` to "peek" the current status of a node.  It returns a structure with the following:

```
function getInfo_UNSTABLE(RecoilValue): {
    loadable: ?Loadable<T>,
    isActive: boolean,
    isSet: boolean,
    isModified: boolean,
    type: 'atom' | 'selector' | void,
    deps: Iterable<RecoilValue<mixed>>,
    subscribers: {
      nodes: Iterable<RecoilValue<mixed>>,
    },
}
```

This API could replace both `getDeps()` and `getSubscribers()` if we decide we prefer this approach.

There is a semantic difference that this only "peeks" the state and will not evaluate selectors that have not already been evaluated or change the snapshot in any way, unlike the current `getDeps()`.  Based on needs for dev tools we can decide if we prefer this or change it to `getStatus()` and hopefully remove the other...

Reviewed By: maxijb, davidmccabe

Differential Revision: D22204282

fbshipit-source-id: a6012ed81db476c74241ffaffae961ec96742836
  • Loading branch information
drarmstr authored and facebook-github-bot committed Jul 31, 2020
1 parent f8f6b4b commit f77ddc9
Show file tree
Hide file tree
Showing 9 changed files with 320 additions and 27 deletions.
8 changes: 3 additions & 5 deletions src/core/Recoil_FunctionalCore.js
Expand Up @@ -43,15 +43,13 @@ function getNodeLoadable<T>(
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<T>(
store: Store,
state: TreeState,
key: NodeKey,
): Loadable<T> {
return getNodeLoadable(store, state, key)[1];
): ?Loadable<T> {
return getNode(key).peek(store, state);
}

// Write value directly to state bypassing the Node interface as the node
Expand Down
3 changes: 3 additions & 0 deletions src/core/Recoil_Node.js
Expand Up @@ -39,6 +39,9 @@ export type PersistenceInfo = $ReadOnly<{
export type ReadOnlyNodeOptions<T> = $ReadOnly<{
key: NodeKey,

// 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) => [DependencyMap, Loadable<T>],

Expand Down
5 changes: 2 additions & 3 deletions src/core/Recoil_RecoilValueInterface.js
Expand Up @@ -25,7 +25,6 @@ const Tracing = require('../util/Recoil_Tracing');
const unionSets = require('../util/Recoil_unionSets');
const {
getNodeLoadable,
peekNodeLoadable,
setNodeValue,
setUnvalidatedAtomValue,
} = require('./Recoil_FunctionalCore');
Expand Down Expand Up @@ -74,8 +73,8 @@ function valueFromValueOrUpdater<T>(
// 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') {
Expand Down
57 changes: 52 additions & 5 deletions src/core/Recoil_Snapshot.js
Expand Up @@ -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 {
Expand Down Expand Up @@ -130,10 +133,13 @@ class Snapshot {
recoilValue: RecoilValue<T>,
) => {
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));
Expand Down Expand Up @@ -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: <T>(
RecoilValue<T>,
) => {
loadable: ?Loadable<T>,
isActive: boolean,
isSet: boolean,
isModified: boolean, // TODO report modified selectors
type: 'atom' | 'selector' | void, // void until initialized for now
deps: Iterable<RecoilValue<mixed>>,
subscribers: {
nodes: Iterable<RecoilValue<mixed>>,
},
} = <T>(recoilValue: RecoilValue<T>) => {
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);
Expand Down
190 changes: 186 additions & 4 deletions src/core/__tests__/Recoil_Snapshot-test.js
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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<string>({
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([]);
});
4 changes: 2 additions & 2 deletions src/core/__tests__/Recoil_core-test.js
Expand Up @@ -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<number>({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,
Expand Down

0 comments on commit f77ddc9

Please sign in to comment.