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,328 @@
/**
* 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,
RecoilValueReadOnly,
} from '../core/Recoil_RecoilValue';
import type {GetRecoilValue} from './Recoil_callbackTypes';
const {
loadableWithError,
loadableWithPromise,
loadableWithValue,
} = require('../adt/Recoil_Loadable');
const selector = require('./Recoil_selector');
const selectorFamily = require('./Recoil_selectorFamily');
const isPromise = require('recoil-shared/util/Recoil_isPromise');
/////////////////
// TRUTH TABLE
/////////////////
// Dependencies waitForNone waitForAny waitForAll waitForAllSettled
// [loading, loading] [Promise, Promise] Promise Promise Promise
// [value, loading] [value, Promise] [value, Promise] Promise Promise
// [value, value] [value, value] [value, value] [value, value] [value, value]
//
// [error, loading] [Error, Promise] [Error, Promise] Error Promise
// [error, error] [Error, Error] [Error, Error] Error [error, error]
// [value, error] [value, Error] [value, Error] Error [value, error]
// Issue parallel requests for all dependencies and return the current
// status if they have results, have some error, or are still pending.
function concurrentRequests(
getRecoilValue: GetRecoilValue,
deps: $ReadOnlyArray<RecoilValue<mixed>>,
) {
const results = Array(deps.length).fill(undefined);
const exceptions = Array(deps.length).fill(undefined);
for (const [i, dep] of deps.entries()) {
try {
results[i] = getRecoilValue(dep);
} catch (e) {
// exceptions can either be Promises of pending results or real errors
exceptions[i] = e;
}
}
return [results, exceptions];
}
function isError(exp: $FlowFixMe) {
return exp != null && !isPromise(exp);
}
function unwrapDependencies(
dependencies:
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>
| {+[string]: RecoilValueReadOnly<mixed>},
): $ReadOnlyArray<RecoilValue<mixed>> {
return Array.isArray(dependencies)
? dependencies
: Object.getOwnPropertyNames(dependencies).map(key => dependencies[key]);
}
function wrapResults(
dependencies:
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>
| {+[string]: RecoilValueReadOnly<mixed>},
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
results,
) {
return Array.isArray(dependencies)
? results
: // Object.getOwnPropertyNames() has consistent key ordering with ES6
Object.getOwnPropertyNames(dependencies).reduce(
(out, key, idx) => ({...out, [(key: string)]: results[idx]}),
{},
);
}
function wrapLoadables(
dependencies:
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>
| {+[string]: RecoilValueReadOnly<mixed>},
results: Array<$FlowFixMe>,
exceptions: Array<$FlowFixMe>,
) {
const output = exceptions.map((exception, idx) =>
exception == null
? loadableWithValue(results[idx])
: isPromise(exception)
? loadableWithPromise(exception)
: loadableWithError<mixed>(exception),
);
return wrapResults(dependencies, output);
}
function combineAsyncResultsWithSyncResults<T>(
syncResults: Array<T>,
asyncResults: Array<T>,
): Array<T> {
return asyncResults.map((result, idx) =>
/**
* it's important we use === undefined as opposed to == null, because the
* resolved value of the async promise could be `null`, in which case we
* don't want to use syncResults[idx], which would be undefined. If async
* promise resolves to `undefined`, that's ok because `syncResults[idx]`
* will also be `undefined`. That's a little hacky, but it works.
*/
result === undefined ? syncResults[idx] : result,
);
}
// Selector that requests all dependencies in parallel and immediately returns
// current results without waiting.
const waitForNone: <
RecoilValues:
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>
| $ReadOnly<{[string]: RecoilValueReadOnly<mixed>, ...}>,
>(
RecoilValues,
) => RecoilValueReadOnly<
$ReadOnlyArray<Loadable<mixed>> | $ReadOnly<{[string]: Loadable<mixed>, ...}>,
> = selectorFamily({
key: '__waitForNone',
get:
(
dependencies:
| $ReadOnly<{[string]: RecoilValueReadOnly<mixed>}>
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>,
) =>
({get}) => {
// Issue requests for all dependencies in parallel.
const deps = unwrapDependencies(dependencies);
const [results, exceptions] = concurrentRequests(get, deps);
// Always return the current status of the results; never block.
return wrapLoadables(dependencies, results, exceptions);
},
dangerouslyAllowMutability: true,
});
// Selector that requests all dependencies in parallel and waits for at least
// one to be available before returning results. It will only error if all
// dependencies have errors.
const waitForAny: <
RecoilValues:
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>
| $ReadOnly<{[string]: RecoilValueReadOnly<mixed>, ...}>,
>(
RecoilValues,
) => RecoilValueReadOnly<
$ReadOnlyArray<mixed> | $ReadOnly<{[string]: mixed, ...}>,
> = selectorFamily({
key: '__waitForAny',
get:
(
dependencies:
| $ReadOnly<{[string]: RecoilValueReadOnly<mixed>}>
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>,
) =>
({get}) => {
// Issue requests for all dependencies in parallel.
// Exceptions can either be Promises of pending results or real errors
const deps = unwrapDependencies(dependencies);
const [results, exceptions] = concurrentRequests(get, deps);
// If any results are available, value or error, return the current status
if (exceptions.some(exp => !isPromise(exp))) {
return wrapLoadables(dependencies, results, exceptions);
}
// Otherwise, return a promise that will resolve when the next result is
// available, whichever one happens to be next. But, if all pending
// dependencies end up with errors, then reject the promise.
return new Promise(resolve => {
for (const [i, exp] of exceptions.entries()) {
if (isPromise(exp)) {
exp
.then(result => {
results[i] = result;
exceptions[i] = undefined;
resolve(wrapLoadables(dependencies, results, exceptions));
})
.catch(error => {
exceptions[i] = error;
resolve(wrapLoadables(dependencies, results, exceptions));
});
}
}
});
},
dangerouslyAllowMutability: true,
});
// Selector that requests all dependencies in parallel and waits for all to be
// available before returning a value. It will error if any dependencies error.
const waitForAll: <
RecoilValues:
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>
| $ReadOnly<{[string]: RecoilValueReadOnly<mixed>, ...}>,
>(
RecoilValues,
) => RecoilValueReadOnly<
$ReadOnlyArray<mixed> | $ReadOnly<{[string]: mixed, ...}>,
> = selectorFamily({
key: '__waitForAll',
get:
(
dependencies:
| $ReadOnly<{[string]: RecoilValueReadOnly<mixed>}>
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>,
) =>
({get}) => {
// Issue requests for all dependencies in parallel.
// Exceptions can either be Promises of pending results or real errors
const deps = unwrapDependencies(dependencies);
const [results, exceptions] = concurrentRequests(get, deps);
// If all results are available, return the results
if (exceptions.every(exp => exp == null)) {
return wrapResults(dependencies, results);
}
// If we have any errors, throw the first error
const error = exceptions.find(isError);
if (error != null) {
throw error;
}
// Otherwise, return a promise that will resolve when all results are available
return Promise.all(exceptions).then(exceptionResults =>
wrapResults(
dependencies,
combineAsyncResultsWithSyncResults(results, exceptionResults),
),
);
},
dangerouslyAllowMutability: true,
});
const waitForAllSettled: <
RecoilValues:
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>
| $ReadOnly<{[string]: RecoilValueReadOnly<mixed>, ...}>,
>(
RecoilValues,
) => RecoilValueReadOnly<
$ReadOnlyArray<mixed> | $ReadOnly<{[string]: mixed, ...}>,
> = selectorFamily({
key: '__waitForAllSettled',
get:
(
dependencies:
| $ReadOnly<{[string]: RecoilValueReadOnly<mixed>}>
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>,
) =>
({get}) => {
// Issue requests for all dependencies in parallel.
// Exceptions can either be Promises of pending results or real errors
const deps = unwrapDependencies(dependencies);
const [results, exceptions] = concurrentRequests(get, deps);
// If all results are available, return the results
if (exceptions.every(exp => !isPromise(exp))) {
return wrapLoadables(dependencies, results, exceptions);
}
// Wait for all results to settle
return (
Promise.all(
exceptions.map((exp, i) =>
isPromise(exp)
? exp
.then(result => {
results[i] = result;
exceptions[i] = undefined;
})
.catch(error => {
results[i] = undefined;
exceptions[i] = error;
})
: null,
),
)
// Then wrap them as loadables
.then(() => wrapLoadables(dependencies, results, exceptions))
);
},
dangerouslyAllowMutability: true,
});
const noWait: (RecoilValue<mixed>) => RecoilValueReadOnly<Loadable<mixed>> =
selectorFamily({
key: '__noWait',
get:
dependency =>
({get}) => {
try {
return selector.value(loadableWithValue(get(dependency)));
} catch (exception) {
return selector.value(
isPromise(exception)
? loadableWithPromise(exception)
: loadableWithError(exception),
);
}
},
dangerouslyAllowMutability: true,
});
module.exports = {
waitForNone,
waitForAny,
waitForAll,
waitForAllSettled,
noWait,
};

View File

@@ -0,0 +1,697 @@
/**
* 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.
*
* Returns an atom, the basic unit of state in Recoil. An atom is a reference to
* value that can be read, written, and subscribed to. It has a `key` that is
* stable across time so that it can be persisted.
*
* There are two required options for creating an atom:
*
* key. This is a string that uniquely identifies the atom. It should be
* stable across time so that persisted states remain valid.
*
* default
* If `default` is provided, the atom is initialized to that value.
* Or, it may be set to another RecoilValue to use as a fallback.
* In that case, the value of the atom will be equal to that of the
* fallback, and will remain so until the first time the atom is written
* to, at which point it will stop tracking the fallback RecoilValue.
*
* The `persistence` option specifies that the atom should be saved to storage.
* It is an object with two properties: `type` specifies where the atom should
* be persisted; its only allowed value is "url"; `backButton` specifies whether
* changes to the atom should result in pushes to the browser history stack; if
* true, changing the atom and then hitting the Back button will cause the atom's
* previous value to be restored. Applications are responsible for implementing
* persistence by using the `useTransactionObservation` hook.
*
* Scoped atoms (DEPRECATED):
* ===================================================================================
*
* The scopeRules_APPEND_ONLY_READ_THE_DOCS option causes the atom be be "scoped".
* A scoped atom's value depends on other parts of the application state.
* A separate value of the atom is stored for every value of the state that it
* depends on. The dependencies may be changed without breaking existing URLs --
* it uses whatever rule was current when the URL was written. Values written
* under the newer rules are overlaid atop the previously-written values just for
* those states in which the write occurred, with reads falling back to the older
* values in other states, and eventually to the default or fallback.
*
* The scopedRules_APPEND_ONLY_READ_THE_DOCS parameter is a list of rules;
* it should start with a single entry. This list must only be appended to:
* existing entries must never be deleted or modified. Each rule is an atom
* or selector whose value is some arbitrary key. A different value of the
* scoped atom will be stored for each key. To change the scope rule, simply add
* a new function to the list. Each rule is either an array of atoms of primitives,
* or an atom of an array of primitives.
*
* Ordinary atoms may be upgraded to scoped atoms. To un-scope an atom, add a new
* scope rule consisting of a constant.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Loadable, LoadingLoadableType} from '../adt/Recoil_Loadable';
import type {RecoilValueInfo} from '../core/Recoil_FunctionalCore';
import type {StoreID} from '../core/Recoil_Keys';
import type {
PersistenceInfo,
ReadWriteNodeOptions,
Trigger,
} from '../core/Recoil_Node';
import type {RecoilState, RecoilValue} from '../core/Recoil_RecoilValue';
import type {RetainedBy} from '../core/Recoil_RetainedBy';
import type {AtomWrites, NodeKey, Store, TreeState} from '../core/Recoil_State';
// @fb-only: import type {ScopeRules} from 'Recoil_ScopedAtom';
// @fb-only: const {scopedAtom} = require('Recoil_ScopedAtom');
const {
isLoadable,
loadableWithError,
loadableWithPromise,
loadableWithValue,
} = require('../adt/Recoil_Loadable');
const {WrappedValue} = require('../adt/Recoil_Wrapper');
const {peekNodeInfo} = require('../core/Recoil_FunctionalCore');
const {
DEFAULT_VALUE,
DefaultValue,
getConfigDeletionHandler,
registerNode,
setConfigDeletionHandler,
} = require('../core/Recoil_Node');
const {isRecoilValue} = require('../core/Recoil_RecoilValue');
const {
getRecoilValueAsLoadable,
markRecoilValueModified,
setRecoilValue,
setRecoilValueLoadable,
} = require('../core/Recoil_RecoilValueInterface');
const {retainedByOptionWithDefault} = require('../core/Recoil_Retention');
const selector = require('./Recoil_selector');
const deepFreezeValue = require('recoil-shared/util/Recoil_deepFreezeValue');
const err = require('recoil-shared/util/Recoil_err');
const expectationViolation = require('recoil-shared/util/Recoil_expectationViolation');
const isPromise = require('recoil-shared/util/Recoil_isPromise');
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
export type PersistenceSettings<Stored> = $ReadOnly<{
...PersistenceInfo,
validator: (mixed, DefaultValue) => Stored | DefaultValue,
}>;
// TODO Support Loadable<T>
type NewValue<T> =
| T
| DefaultValue
| Promise<T | DefaultValue>
| WrappedValue<T>;
type NewValueOrUpdater<T> =
| T
| DefaultValue
| Promise<T | DefaultValue>
| WrappedValue<T>
| ((T | DefaultValue) => T | DefaultValue | WrappedValue<T>);
// Effect is called the first time a node is used with a <RecoilRoot>
export type AtomEffect<T> = ({
node: RecoilState<T>,
storeID: StoreID,
parentStoreID_UNSTABLE?: StoreID,
trigger: Trigger,
// Call synchronously to initialize value or async to change it later
setSelf: (
| T
| DefaultValue
| Promise<T | DefaultValue>
| WrappedValue<T>
| ((T | DefaultValue) => T | DefaultValue | WrappedValue<T>),
) => void,
resetSelf: () => void,
// Subscribe callbacks to events.
// Atom effect observers are called before global transaction observers
onSet: (
(newValue: T, oldValue: T | DefaultValue, isReset: boolean) => void,
) => void,
// Accessors to read other atoms/selectors
getPromise: <S>(RecoilValue<S>) => Promise<S>,
getLoadable: <S>(RecoilValue<S>) => Loadable<S>,
getInfo_UNSTABLE: <S>(RecoilValue<S>) => RecoilValueInfo<S>,
}) => void | (() => void);
export type AtomOptionsWithoutDefault<T> = $ReadOnly<{
key: NodeKey,
effects?: $ReadOnlyArray<AtomEffect<T>>,
effects_UNSTABLE?: $ReadOnlyArray<AtomEffect<T>>,
persistence_UNSTABLE?: PersistenceSettings<T>,
// @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS?: ScopeRules,
dangerouslyAllowMutability?: boolean,
retainedBy_UNSTABLE?: RetainedBy,
}>;
type AtomOptionsWithDefault<T> = $ReadOnly<{
...AtomOptionsWithoutDefault<T>,
default: RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T> | T,
}>;
export type AtomOptions<T> =
| AtomOptionsWithDefault<T>
| AtomOptionsWithoutDefault<T>;
type BaseAtomOptions<T> = $ReadOnly<{
...AtomOptions<T>,
default: Promise<T> | Loadable<T> | WrappedValue<T> | T,
}>;
const unwrap = <T, S = T>(x: T | S | WrappedValue<T>): T | S =>
x instanceof WrappedValue ? x.value : x;
function baseAtom<T>(options: BaseAtomOptions<T>): RecoilState<T> {
const {key, persistence_UNSTABLE: persistence} = options;
const retainedBy = retainedByOptionWithDefault(options.retainedBy_UNSTABLE);
let liveStoresCount = 0;
function unwrapPromise(promise: Promise<T>): Loadable<T> {
return loadableWithPromise(
promise
.then(value => {
defaultLoadable = loadableWithValue(value);
return value;
})
.catch(error => {
defaultLoadable = loadableWithError(error);
throw error;
}),
);
}
let defaultLoadable: Loadable<T> = isPromise(options.default)
? unwrapPromise(options.default)
: isLoadable(options.default)
? options.default.state === 'loading'
? unwrapPromise((options.default: LoadingLoadableType<T>).contents)
: options.default
: // $FlowFixMe[incompatible-call]
loadableWithValue(unwrap(options.default));
maybeFreezeValueOrPromise(defaultLoadable.contents);
let cachedAnswerForUnvalidatedValue: void | Loadable<T> = undefined;
// Cleanup handlers for this atom
// Rely on stable reference equality of the store to use it as a key per <RecoilRoot>
const cleanupEffectsByStore: Map<Store, Array<() => void>> = new Map();
function maybeFreezeValueOrPromise(valueOrPromise: mixed) {
if (__DEV__) {
if (options.dangerouslyAllowMutability !== true) {
if (isPromise(valueOrPromise)) {
return valueOrPromise.then(value => {
deepFreezeValue(value);
return value;
});
} else {
deepFreezeValue(valueOrPromise);
return valueOrPromise;
}
}
}
return valueOrPromise;
}
function wrapPendingPromise(
store: Store,
promise: Promise<T | DefaultValue>,
): Promise<T | DefaultValue> {
const wrappedPromise: Promise<T | DefaultValue> = promise
.then(value => {
const state = store.getState().nextTree ?? store.getState().currentTree;
if (state.atomValues.get(key)?.contents === wrappedPromise) {
setRecoilValue(store, node, value);
}
return value;
})
.catch(error => {
const state = store.getState().nextTree ?? store.getState().currentTree;
if (state.atomValues.get(key)?.contents === wrappedPromise) {
setRecoilValueLoadable(store, node, loadableWithError(error));
}
throw error;
});
return wrappedPromise;
}
function initAtom(
store: Store,
initState: TreeState,
trigger: Trigger,
): () => void {
liveStoresCount++;
const cleanupAtom = () => {
liveStoresCount--;
cleanupEffectsByStore.get(store)?.forEach(cleanup => cleanup());
cleanupEffectsByStore.delete(store);
};
store.getState().knownAtoms.add(key);
// Setup async defaults to notify subscribers when they resolve
if (defaultLoadable.state === 'loading') {
const notifyDefaultSubscribers = () => {
const state = store.getState().nextTree ?? store.getState().currentTree;
if (!state.atomValues.has(key)) {
markRecoilValueModified(store, node);
}
};
defaultLoadable.contents.finally(notifyDefaultSubscribers);
}
///////////////////
// Run Atom Effects
///////////////////
const effects = options.effects ?? options.effects_UNSTABLE;
if (effects != null) {
// This state is scoped by Store, since this is in the initAtom() closure
let initValue: NewValue<T> = DEFAULT_VALUE;
let isDuringInit = true;
let isInitError: boolean = false;
let pendingSetSelf: ?{
effect: AtomEffect<T>,
value: T | DefaultValue,
} = null;
function getLoadable<S>(recoilValue: RecoilValue<S>): Loadable<S> {
// Normally we can just get the current value of another atom.
// But for our own value we need to check if there is a pending
// initialized value or get the fallback default value.
if (isDuringInit && recoilValue.key === key) {
// Cast T to S
const retValue: NewValue<S> = (initValue: any); // flowlint-line unclear-type:off
return retValue instanceof DefaultValue
? (peekAtom(store, initState): any) // flowlint-line unclear-type:off
: isPromise(retValue)
? loadableWithPromise(
retValue.then((v: S | DefaultValue): S | Promise<S> =>
v instanceof DefaultValue
? // Cast T to S
(defaultLoadable: any).toPromise() // flowlint-line unclear-type:off
: v,
),
)
: // $FlowFixMe[incompatible-call]
loadableWithValue(retValue);
}
return getRecoilValueAsLoadable(store, recoilValue);
}
function getPromise<S>(recoilValue: RecoilValue<S>): Promise<S> {
return getLoadable(recoilValue).toPromise();
}
function getInfo_UNSTABLE<S>(
recoilValue: RecoilValue<S>,
): RecoilValueInfo<S> {
const info = peekNodeInfo<S>(
store,
store.getState().nextTree ?? store.getState().currentTree,
recoilValue.key,
);
return isDuringInit &&
recoilValue.key === key &&
!(initValue instanceof DefaultValue)
? {...info, isSet: true, loadable: getLoadable(recoilValue)}
: info;
}
const setSelf =
(effect: AtomEffect<T>) => (valueOrUpdater: NewValueOrUpdater<T>) => {
if (isDuringInit) {
const currentLoadable = getLoadable(node);
const currentValue: T | DefaultValue =
currentLoadable.state === 'hasValue'
? currentLoadable.contents
: DEFAULT_VALUE;
initValue =
typeof valueOrUpdater === 'function'
? // cast to any because we can't restrict T from being a function without losing support for opaque types
(valueOrUpdater: any)(currentValue) // flowlint-line unclear-type:off
: valueOrUpdater;
if (isPromise(initValue)) {
initValue = initValue.then(value => {
// Avoid calling onSet() when setSelf() initializes with a Promise
pendingSetSelf = {effect, value};
return value;
});
}
} else {
if (isPromise(valueOrUpdater)) {
throw err('Setting atoms to async values is not implemented.');
}
if (typeof valueOrUpdater !== 'function') {
pendingSetSelf = {
effect,
value: unwrap<T, DefaultValue>(valueOrUpdater),
};
}
setRecoilValue(
store,
node,
typeof valueOrUpdater === 'function'
? (currentValue: $FlowFixMe) => {
const newValue = unwrap(
// cast to any because we can't restrict T from being a function without losing support for opaque types
(valueOrUpdater: any)(currentValue), // flowlint-line unclear-type:off
);
// $FlowFixMe[incompatible-type]
pendingSetSelf = {effect, value: newValue};
return newValue;
}
: unwrap(valueOrUpdater),
);
}
};
const resetSelf = (effect: AtomEffect<T>) => () =>
setSelf(effect)(DEFAULT_VALUE);
const onSet =
(effect: AtomEffect<T>) =>
(handler: (T, T | DefaultValue, boolean) => void) => {
const {release} = store.subscribeToTransactions(currentStore => {
// eslint-disable-next-line prefer-const
let {currentTree, previousTree} = currentStore.getState();
if (!previousTree) {
recoverableViolation(
'Transaction subscribers notified without a next tree being present -- this is a bug in Recoil',
'recoil',
);
previousTree = currentTree; // attempt to trundle on
}
const newLoadable =
currentTree.atomValues.get(key) ?? defaultLoadable;
if (newLoadable.state === 'hasValue') {
const newValue: T = newLoadable.contents;
const oldLoadable =
previousTree.atomValues.get(key) ?? defaultLoadable;
const oldValue: T | DefaultValue =
oldLoadable.state === 'hasValue'
? oldLoadable.contents
: DEFAULT_VALUE; // TODO This isn't actually valid, use as a placeholder for now.
// Ignore atom value changes that were set via setSelf() in the same effect.
// We will still properly call the handler if there was a subsequent
// set from something other than an atom effect which was batched
// with the `setSelf()` call. However, we may incorrectly ignore
// the handler if the subsequent batched call happens to set the
// atom to the exact same value as the `setSelf()`. But, in that
// case, it was kind of a noop, so the semantics are debatable..
if (
pendingSetSelf?.effect !== effect ||
pendingSetSelf?.value !== newValue
) {
handler(newValue, oldValue, !currentTree.atomValues.has(key));
} else if (pendingSetSelf?.effect === effect) {
pendingSetSelf = null;
}
}
}, key);
cleanupEffectsByStore.set(store, [
...(cleanupEffectsByStore.get(store) ?? []),
release,
]);
};
for (const effect of effects) {
try {
const cleanup = effect({
node,
storeID: store.storeID,
parentStoreID_UNSTABLE: store.parentStoreID,
trigger,
setSelf: setSelf(effect),
resetSelf: resetSelf(effect),
onSet: onSet(effect),
getPromise,
getLoadable,
getInfo_UNSTABLE,
});
if (cleanup != null) {
cleanupEffectsByStore.set(store, [
...(cleanupEffectsByStore.get(store) ?? []),
cleanup,
]);
}
} catch (error) {
initValue = error;
isInitError = true;
}
}
isDuringInit = false;
// Mutate initial state in place since we know there are no other subscribers
// since we are the ones initializing on first use.
if (!(initValue instanceof DefaultValue)) {
const initLoadable = isInitError
? loadableWithError<$FlowFixMe>(initValue)
: isPromise(initValue)
? loadableWithPromise(wrapPendingPromise(store, initValue))
: loadableWithValue(unwrap(initValue));
maybeFreezeValueOrPromise(initLoadable.contents);
initState.atomValues.set(key, initLoadable);
// If there is a pending transaction, then also mutate the next state tree.
// This could happen if the atom was first initialized in an action that
// also updated some other atom's state.
store.getState().nextTree?.atomValues.set(key, initLoadable);
}
}
return cleanupAtom;
}
function peekAtom(_store: Store, state: TreeState): Loadable<T> {
return (
state.atomValues.get(key) ??
cachedAnswerForUnvalidatedValue ??
defaultLoadable
);
}
function getAtom(_store: Store, state: TreeState): Loadable<T> {
if (state.atomValues.has(key)) {
// Atom value is stored in state:
return nullthrows(state.atomValues.get(key));
} 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 != null) {
return cachedAnswerForUnvalidatedValue;
}
if (persistence == null) {
expectationViolation(
`Tried to restore a persisted value for atom ${key} but it has no persistence settings.`,
);
return defaultLoadable;
}
const nonvalidatedValue = state.nonvalidatedAtoms.get(key);
const validatorResult: T | DefaultValue = persistence.validator(
nonvalidatedValue,
DEFAULT_VALUE,
);
const validatedValueLoadable =
validatorResult instanceof DefaultValue
? defaultLoadable
: loadableWithValue(validatorResult);
cachedAnswerForUnvalidatedValue = validatedValueLoadable;
return cachedAnswerForUnvalidatedValue;
} else {
return defaultLoadable;
}
}
function invalidateAtom() {
cachedAnswerForUnvalidatedValue = undefined;
}
function setAtom(
_store: Store,
state: TreeState,
newValue: T | DefaultValue,
): AtomWrites {
// Bail out if we're being set to the existing value, or if we're being
// reset but have no stored value (validated or unvalidated) to reset from:
if (state.atomValues.has(key)) {
const existing = nullthrows(state.atomValues.get(key));
if (existing.state === 'hasValue' && newValue === existing.contents) {
return new Map();
}
} else if (
!state.nonvalidatedAtoms.has(key) &&
newValue instanceof DefaultValue
) {
return new Map();
}
maybeFreezeValueOrPromise(newValue);
cachedAnswerForUnvalidatedValue = undefined; // can be released now if it was previously in use
return new Map<NodeKey, Loadable<$FlowFixMe>>().set(
key,
loadableWithValue(newValue),
);
}
function shouldDeleteConfigOnReleaseAtom() {
return getConfigDeletionHandler(key) !== undefined && liveStoresCount <= 0;
}
const node = registerNode(
({
key,
nodeType: 'atom',
peek: peekAtom,
get: getAtom,
set: setAtom,
init: initAtom,
invalidate: invalidateAtom,
shouldDeleteConfigOnRelease: shouldDeleteConfigOnReleaseAtom,
dangerouslyAllowMutability: options.dangerouslyAllowMutability,
persistence_UNSTABLE: options.persistence_UNSTABLE
? {
type: options.persistence_UNSTABLE.type,
backButton: options.persistence_UNSTABLE.backButton,
}
: undefined,
shouldRestoreFromSnapshots: true,
retainedBy,
}: ReadWriteNodeOptions<T>),
);
return node;
}
// prettier-ignore
function atom<T>(options: AtomOptions<T>): RecoilState<T> {
if (__DEV__) {
if (typeof options.key !== 'string') {
throw err(
'A key option with a unique string value must be provided when creating an atom.',
);
}
}
const {
// @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS,
...restOptions
} = options;
const optionsDefault: RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T> | T =
'default' in options
?
// $FlowIssue[incompatible-type] No way to refine in Flow that property is not defined
options.default
: new Promise(() => {});
if (isRecoilValue(optionsDefault)
// Continue to use atomWithFallback for promise defaults for scoped atoms
// for now, since scoped atoms don't support async defaults
// @fb-only: || (isPromise(optionsDefault) && scopeRules_APPEND_ONLY_READ_THE_DOCS)
// @fb-only: || (isLoadable(optionsDefault) && scopeRules_APPEND_ONLY_READ_THE_DOCS)
) {
return atomWithFallback<T>({
...restOptions,
default: optionsDefault,
// @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS,
});
// @fb-only: } else if (scopeRules_APPEND_ONLY_READ_THE_DOCS
// @fb-only: && !isPromise(optionsDefault)
// @fb-only: && !isLoadable(optionsDefault)
// @fb-only: ) {
// @fb-only: return scopedAtom<T>({
// @fb-only: ...restOptions,
// @fb-only: default: unwrap<T>(optionsDefault),
// @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS,
// @fb-only: });
} else {
return baseAtom<T>({...restOptions, default: optionsDefault});
}
}
type AtomWithFallbackOptions<T> = $ReadOnly<{
...AtomOptions<T>,
default: RecoilValue<T> | Promise<T> | Loadable<T>,
}>;
function atomWithFallback<T>(
options: AtomWithFallbackOptions<T>,
): RecoilState<T> {
const base = atom<T | DefaultValue>({
...options,
default: DEFAULT_VALUE,
persistence_UNSTABLE:
options.persistence_UNSTABLE === undefined
? undefined
: {
...options.persistence_UNSTABLE,
validator: (storedValue: mixed) =>
storedValue instanceof DefaultValue
? storedValue
: nullthrows(options.persistence_UNSTABLE).validator(
storedValue,
DEFAULT_VALUE,
),
},
// TODO Hack for now.
effects: (options.effects: any), // flowlint-line unclear-type: off
effects_UNSTABLE: (options.effects_UNSTABLE: any), // flowlint-line unclear-type: off
});
// $FlowFixMe[incompatible-call]
const sel = selector<T>({
key: `${options.key}__withFallback`,
get: ({get}) => {
const baseValue = get(base);
return baseValue instanceof DefaultValue ? options.default : baseValue;
},
// $FlowFixMe[incompatible-call]
set: ({set}, newValue) => set(base, newValue),
// This selector does not need to cache as it is a wrapper selector
// and the selector within the wrapper selector will have a cache
// option by default
cachePolicy_UNSTABLE: {
eviction: 'most-recent',
},
dangerouslyAllowMutability: options.dangerouslyAllowMutability,
});
setConfigDeletionHandler(sel.key, getConfigDeletionHandler(options.key));
return sel;
}
// $FlowFixMe[missing-local-annot]
atom.value = value => new WrappedValue(value);
module.exports = (atom: {
<T>(AtomOptions<T>): RecoilState<T>,
value: <S>(S) => WrappedValue<S>,
});

View File

@@ -0,0 +1,176 @@
/**
* 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 {WrappedValue} from '../adt/Recoil_Wrapper';
import type {CachePolicyWithoutEviction} from '../caches/Recoil_CachePolicy';
import type {RecoilState, RecoilValue} from '../core/Recoil_RecoilValue';
import type {RetainedBy} from '../core/Recoil_RetainedBy';
import type {AtomEffect, AtomOptionsWithoutDefault} from './Recoil_atom';
// @fb-only: import type {ScopeRules} from 'Recoil_ScopedAtom';
const cacheFromPolicy = require('../caches/Recoil_cacheFromPolicy');
const {setConfigDeletionHandler} = require('../core/Recoil_Node');
const atom = require('./Recoil_atom');
const stableStringify = require('recoil-shared/util/Recoil_stableStringify');
type Primitive = void | null | boolean | number | string;
interface HasToJSON {
toJSON(): Parameter;
}
export type Parameter =
| Primitive
| HasToJSON
| $ReadOnlyArray<Parameter>
| $ReadOnly<{[string]: Parameter}>
| $ReadOnlySet<Parameter>
| $ReadOnlyMap<Parameter, Parameter>;
// flowlint unclear-type:off
export type ParameterizedScopeRules<P> = $ReadOnlyArray<
| RecoilValue<$ReadOnlyArray<any>>
| $ReadOnlyArray<RecoilValue<any> | (P => RecoilValue<any>)>,
>;
// flowlint unclear-type:error
export type AtomFamilyOptionsWithoutDefault<T, P: Parameter> = $ReadOnly<{
...AtomOptionsWithoutDefault<T>,
effects?:
| $ReadOnlyArray<AtomEffect<T>>
| (P => $ReadOnlyArray<AtomEffect<T>>),
// effects_UNSTABLE?:
// | $ReadOnlyArray<AtomEffect<T>>
// | (P => $ReadOnlyArray<AtomEffect<T>>),
retainedBy_UNSTABLE?: RetainedBy | (P => RetainedBy),
cachePolicyForParams_UNSTABLE?: CachePolicyWithoutEviction,
// @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS?: ParameterizedScopeRules<P>,
}>;
export type AtomFamilyOptions<T, P: Parameter> =
| $ReadOnly<{
...AtomFamilyOptionsWithoutDefault<T, P>,
default:
| RecoilValue<T>
| Promise<T>
| Loadable<T>
| WrappedValue<T>
| T
| (P =>
| T
| RecoilValue<T>
| Promise<T>
| Loadable<T>
| WrappedValue<T>),
}>
| AtomFamilyOptionsWithoutDefault<T, P>;
// Process scopeRules to handle any entries which are functions taking parameters
// prettier-ignore
// @fb-only: function mapScopeRules<P>(
// @fb-only: scopeRules?: ParameterizedScopeRules<P>,
// @fb-only: param: P,
// @fb-only: ): ScopeRules | void {
// @fb-only: return scopeRules?.map(rule =>
// @fb-only: Array.isArray(rule)
// @fb-only: ? rule.map(entry => (typeof entry === 'function' ? entry(param) : entry))
// @fb-only: : rule,
// @fb-only: );
// @fb-only: }
/*
A function which returns an atom based on the input parameter.
Each unique parameter returns a unique atom. E.g.,
const f = atomFamily(...);
f({a: 1}) => an atom
f({a: 2}) => a different atom
This allows components to persist local, private state using atoms. Each
instance of the component may have a different key, which it uses as the
parameter for a family of atoms; in this way, each component will have
its own atom not shared by other instances. These state keys may be composed
into children's state keys as well.
*/
function atomFamily<T, P: Parameter>(
options: AtomFamilyOptions<T, P>,
): P => RecoilState<T> {
const atomCache = cacheFromPolicy<P, RecoilState<T>>({
equality: options.cachePolicyForParams_UNSTABLE?.equality ?? 'value',
eviction: 'keep-all',
});
// Simple atomFamily implementation to cache individual atoms based
// on the parameter value equality.
return (params: P) => {
const cachedAtom = atomCache.get(params);
if (cachedAtom != null) {
return cachedAtom;
}
const {cachePolicyForParams_UNSTABLE, ...atomOptions} = options;
const optionsDefault:
| RecoilValue<T>
| Promise<T>
| Loadable<T>
| WrappedValue<T>
| T
| (P => T | RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T>) =
'default' in options
? // $FlowIssue[incompatible-type] No way to refine in Flow that property is not defined
options.default
: new Promise(() => {});
const newAtom = atom<T>({
...atomOptions,
key: `${options.key}__${stableStringify(params) ?? 'void'}`,
default:
typeof optionsDefault === 'function'
? // The default was parameterized
// Flow doesn't know that T isn't a function, so we need to case to any
// $FlowIssue[incompatible-use]
optionsDefault(params)
: // Default may be a static value, promise, or RecoilValue
optionsDefault,
retainedBy_UNSTABLE:
typeof options.retainedBy_UNSTABLE === 'function'
? options.retainedBy_UNSTABLE(params)
: options.retainedBy_UNSTABLE,
effects:
typeof options.effects === 'function'
? options.effects(params)
: typeof options.effects_UNSTABLE === 'function'
? options.effects_UNSTABLE(params)
: options.effects ?? options.effects_UNSTABLE,
// prettier-ignore
// @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS: mapScopeRules(
// @fb-only: options.scopeRules_APPEND_ONLY_READ_THE_DOCS,
// @fb-only: params,
// @fb-only: ),
});
atomCache.set(params, newAtom);
setConfigDeletionHandler(newAtom.key, () => {
atomCache.delete(params);
});
return newAtom;
};
}
module.exports = atomFamily;

View File

@@ -0,0 +1,25 @@
/**
* 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 {DefaultValue} from '../core/Recoil_Node';
import type {RecoilState, RecoilValue} from '../core/Recoil_RecoilValue';
export type ValueOrUpdater<T> =
| T
| DefaultValue
| ((prevValue: T) => T | DefaultValue);
export type GetRecoilValue = <T>(RecoilValue<T>) => T;
export type SetRecoilState = <T>(RecoilState<T>, ValueOrUpdater<T>) => void;
export type ResetRecoilState = <T>(RecoilState<T>) => void;
module.exports = ({}: {...});

View File

@@ -0,0 +1,36 @@
/**
* 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 {RecoilValueReadOnly} from '../core/Recoil_RecoilValue';
import type {Parameter} from './Recoil_selectorFamily';
const selectorFamily = require('./Recoil_selectorFamily');
// flowlint-next-line unclear-type:off
const constantSelector = selectorFamily<any, any>({
key: '__constant',
get: constant => () => constant,
cachePolicyForParams_UNSTABLE: {
equality: 'reference',
},
});
// Function that returns a selector which always produces the
// same constant value. It may be called multiple times with the
// same value, based on reference equality, and will provide the
// same selector.
function constSelector<T: Parameter>(constant: T): RecoilValueReadOnly<T> {
return constantSelector(constant);
}
module.exports = constSelector;

View File

@@ -0,0 +1,37 @@
/**
* 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 {RecoilValueReadOnly} from '../core/Recoil_RecoilValue';
const selectorFamily = require('./Recoil_selectorFamily');
const err = require('recoil-shared/util/Recoil_err');
// flowlint-next-line unclear-type:off
const throwingSelector = selectorFamily<any, any>({
key: '__error',
get: message => () => {
throw err(message);
},
// TODO Why?
cachePolicyForParams_UNSTABLE: {
equality: 'reference',
},
});
// Function that returns a selector which always throws an error
// with the provided message.
function errorSelector<T>(message: string): RecoilValueReadOnly<T> {
return throwingSelector(message);
}
module.exports = errorSelector;

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.
*
* Wraps another recoil value and prevents writing to it.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {
RecoilValue,
RecoilValueReadOnly,
} from '../core/Recoil_RecoilValue';
function readOnlySelector<T>(atom: RecoilValue<T>): RecoilValueReadOnly<T> {
// flowlint-next-line unclear-type: off
return (atom: any);
}
module.exports = readOnlySelector;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,194 @@
/**
* 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 {WrappedValue} from '../adt/Recoil_Wrapper';
import type {
CachePolicy,
CachePolicyWithoutEviction,
} from '../caches/Recoil_CachePolicy';
import type {DefaultValue} from '../core/Recoil_Node';
import type {
RecoilState,
RecoilValue,
RecoilValueReadOnly,
} from '../core/Recoil_RecoilValue';
import type {RetainedBy} from '../core/Recoil_RetainedBy';
import type {GetCallback} from '../recoil_values/Recoil_selector';
import type {
GetRecoilValue,
ResetRecoilState,
SetRecoilState,
} from './Recoil_callbackTypes';
const cacheFromPolicy = require('../caches/Recoil_cacheFromPolicy');
const {setConfigDeletionHandler} = require('../core/Recoil_Node');
const selector = require('./Recoil_selector');
const err = require('recoil-shared/util/Recoil_err');
const stableStringify = require('recoil-shared/util/Recoil_stableStringify');
// Keep in mind the parameter needs to be serializable as a cahche key
// using Recoil_stableStringify
type Primitive = void | null | boolean | number | string;
interface HasToJSON {
toJSON(): Parameter;
}
export type Parameter =
| Primitive
| HasToJSON
| $ReadOnlySet<Parameter>
| $ReadOnlyMap<Parameter, Parameter>
| $ReadOnlyArray<Parameter>
| $ReadOnly<{...}>;
// | $ReadOnly<{[string]: Parameter}>; // TODO Better enforce object is serializable
type BaseSelectorFamilyOptions<P: Parameter> = $ReadOnly<{
key: string,
cachePolicyForParams_UNSTABLE?: CachePolicyWithoutEviction,
cachePolicy_UNSTABLE?: CachePolicy,
dangerouslyAllowMutability?: boolean,
retainedBy_UNSTABLE?: RetainedBy | (P => RetainedBy),
}>;
export type ReadOnlySelectorFamilyOptions<T, P: Parameter> = $ReadOnly<{
...BaseSelectorFamilyOptions<P>,
get: P => ({
get: GetRecoilValue,
getCallback: GetCallback<T>,
}) => Promise<T> | Loadable<T> | WrappedValue<T> | RecoilValue<T> | T,
}>;
export type ReadWriteSelectorFamilyOptions<T, P: Parameter> = $ReadOnly<{
...ReadOnlySelectorFamilyOptions<T, P>,
set: P => (
{set: SetRecoilState, get: GetRecoilValue, reset: ResetRecoilState},
newValue: T | DefaultValue,
) => void,
}>;
export type SelectorFamilyOptions<T, P> =
| ReadOnlySelectorFamilyOptions<T, P>
| ReadWriteSelectorFamilyOptions<T, P>;
// Add a unique index to each selector in case the cache implementation allows
// duplicate keys based on equivalent stringified parameters
let nextIndex = 0;
/* eslint-disable no-redeclare */
declare function selectorFamily<T, Params: Parameter>(
options: ReadOnlySelectorFamilyOptions<T, Params>,
): Params => RecoilValueReadOnly<T>;
declare function selectorFamily<T, Params: Parameter>(
options: ReadWriteSelectorFamilyOptions<T, Params>,
): Params => RecoilState<T>;
// Return a function that returns members of a family of selectors of the same type
// E.g.,
//
// const s = selectorFamily(...);
// s({a: 1}) => a selector
// s({a: 2}) => a different selector
//
// By default, the selectors are distinguished by distinct values of the
// parameter based on value equality, not reference equality. This allows using
// object literals or other equivalent objects at callsites to not create
// duplicate cache entries. This behavior may be overridden with the
// cacheImplementationForParams option.
function selectorFamily<T, Params: Parameter>(
options:
| ReadOnlySelectorFamilyOptions<T, Params>
| ReadWriteSelectorFamilyOptions<T, Params>,
): Params => RecoilValue<T> {
const selectorCache = cacheFromPolicy<
Params,
RecoilState<T> | RecoilValueReadOnly<T>,
>({
equality: options.cachePolicyForParams_UNSTABLE?.equality ?? 'value',
eviction: 'keep-all',
});
return (params: Params) => {
// Throw an error with selector key so that it is clear which
// selector is causing an error
let cachedSelector;
try {
cachedSelector = selectorCache.get(params);
} catch (error) {
throw err(
`Problem with cache lookup for selector ${options.key}: ${error.message}`,
);
}
if (cachedSelector != null) {
return cachedSelector;
}
const myKey = `${options.key}__selectorFamily/${
stableStringify(params, {
// It is possible to use functions in parameters if the user uses
// a cache with reference equality thanks to the incrementing index.
allowFunctions: true,
}) ?? 'void'
}/${nextIndex++}`; // Append index in case values serialize to the same key string
const myGet = (callbacks: {
get: GetRecoilValue,
getCallback: GetCallback<T>,
}) => options.get(params)(callbacks);
const myCachePolicy = options.cachePolicy_UNSTABLE;
const retainedBy =
typeof options.retainedBy_UNSTABLE === 'function'
? options.retainedBy_UNSTABLE(params)
: options.retainedBy_UNSTABLE;
let newSelector;
if (options.set != null) {
const set = options.set;
const mySet = (
callbacks: {
get: GetRecoilValue,
reset: ResetRecoilState,
set: SetRecoilState,
},
newValue: T | DefaultValue,
) => set(params)(callbacks, newValue);
newSelector = selector<T>({
key: myKey,
get: myGet,
set: mySet,
cachePolicy_UNSTABLE: myCachePolicy,
dangerouslyAllowMutability: options.dangerouslyAllowMutability,
retainedBy_UNSTABLE: retainedBy,
});
} else {
newSelector = selector<T>({
key: myKey,
get: myGet,
cachePolicy_UNSTABLE: myCachePolicy,
dangerouslyAllowMutability: options.dangerouslyAllowMutability,
retainedBy_UNSTABLE: retainedBy,
});
}
selectorCache.set(params, newSelector);
setConfigDeletionHandler(newSelector.key, () => {
selectorCache.delete(params);
});
return newSelector;
};
}
/* eslint-enable no-redeclare */
module.exports = selectorFamily;

View File

@@ -0,0 +1,135 @@
/**
* 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';
const {useRecoilValue} = require('../../hooks/Recoil_Hooks');
const atom = require('../Recoil_atom');
const readOnlySelector = require('../Recoil_readOnlySelector');
const {
noWait,
waitForAll,
waitForAllSettled,
waitForNone,
} = require('../Recoil_WaitFor');
const numberAtom: RecoilState<number> = atom({key: 'number', default: 0});
const stringAtom: RecoilState<string> = atom({key: 'string', default: ''});
let num: number;
let str: string;
//////////////
// waitForAll
//////////////
// Test tuple unwrapping of types
// eslint-disable-next-line fb-www/react-hooks
const arrayResults = useRecoilValue(
// $FlowIssue[invalid-tuple-map]
waitForAll([readOnlySelector(numberAtom), readOnlySelector(stringAtom)]),
);
num = arrayResults[0];
str = arrayResults[1];
// $FlowExpectedError
num = arrayResults[1];
// $FlowExpectedError
str = arrayResults[0];
// Test object unwrapping of types
// eslint-disable-next-line fb-www/react-hooks
const objResults = useRecoilValue(
// $FlowIssue[invalid-tuple-map]
// $FlowIssue[incompatible-call]
waitForAll({num: numberAtom, str: stringAtom}),
);
num = objResults.num;
str = objResults.str;
// $FlowExpectedError
num = objResults.str;
// $FlowExpectedError
str = objResults.num;
//////////////
// waitForNone
//////////////
// Test tuple unwrapping of types
// eslint-disable-next-line fb-www/react-hooks
const arrayResultsNone = useRecoilValue(
// $FlowIssue[invalid-tuple-map]
waitForNone([readOnlySelector(numberAtom), readOnlySelector(stringAtom)]),
);
num = arrayResultsNone[0].valueOrThrow();
str = arrayResultsNone[1].valueOrThrow();
// $FlowExpectedError
num = arrayResultsNone[1].valueOrThrow();
// $FlowExpectedError
str = arrayResultsNone[0].valueOrThrow();
// Test object unwrapping of types
// eslint-disable-next-line fb-www/react-hooks
const objResultsNone = useRecoilValue(
// $FlowIssue[incompatible-call]
waitForNone({num: numberAtom, str: stringAtom}),
);
num = objResultsNone.num.valueOrThrow();
str = objResultsNone.str.valueOrThrow();
// $FlowExpectedError
num = objResultsNone.str.valueOrThrow();
// $FlowExpectedError
str = objResultsNone.num.valueOrThrow();
//////////////
// waitForAllSettled
//////////////
// Test tuple unwrapping of types
// eslint-disable-next-line fb-www/react-hooks
const arrayResultsAllSettled = useRecoilValue(
waitForAllSettled([
// $FlowIssue[invalid-tuple-map]
readOnlySelector(numberAtom),
// $FlowIssue[invalid-tuple-map]
readOnlySelector(stringAtom),
]),
);
num = arrayResultsAllSettled[0].valueOrThrow();
str = arrayResultsAllSettled[1].valueOrThrow();
// $FlowExpectedError
num = arrayResultsAllSettled[1].valueOrThrow();
// $FlowExpectedError
str = arrayResultsAllSettled[0].valueOrThrow();
// Test object unwrapping of types
// eslint-disable-next-line fb-www/react-hooks
const objResultsAllSettled = useRecoilValue(
// $FlowIssue[invalid-tuple-map]
// $FlowIssue[incompatible-call]
waitForAllSettled({num: numberAtom, str: stringAtom}),
);
num = objResultsAllSettled.num.valueOrThrow();
str = objResultsAllSettled.str.valueOrThrow();
// $FlowExpectedError
num = objResultsAllSettled.str.valueOrThrow();
// $FlowExpectedError
str = objResultsAllSettled.num.valueOrThrow();
//////////////
// noWait
//////////////
num = useRecoilValue(noWait(numberAtom)).valueOrThrow(); // eslint-disable-line fb-www/react-hooks
// $FlowExpectedError
str = useRecoilValue(noWait(numberAtom)).valueOrThrow(); // eslint-disable-line fb-www/react-hooks

View File

@@ -0,0 +1,639 @@
/**
* 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 {RecoilValue} from '../../core/Recoil_RecoilValue';
const {
flushPromisesAndTimers,
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let getRecoilValueAsLoadable,
noWait,
waitForAll,
waitForAllSettled,
waitForAny,
waitForNone,
store,
selector,
invariant;
const testRecoil = getRecoilTestFn(() => {
const {
makeStore,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
invariant = require('recoil-shared/util/Recoil_invariant');
({
getRecoilValueAsLoadable,
} = require('../../core/Recoil_RecoilValueInterface'));
selector = require('../Recoil_selector');
({
noWait,
waitForAll,
waitForAllSettled,
waitForAny,
waitForNone,
} = require('../Recoil_WaitFor'));
store = makeStore();
});
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
function getLoadable(atom) {
return getRecoilValueAsLoadable(store, atom).contents;
}
function getState<T>(
recoilValue: RecoilValue<T>,
): 'loading' | 'hasValue' | 'hasError' {
return getRecoilValueAsLoadable(store, recoilValue).state;
}
function getValue<T>(recoilValue: RecoilValue<T>): T {
const loadable = getRecoilValueAsLoadable(store, recoilValue);
if (loadable.state !== 'hasValue') {
throw new Error(`expected atom "${recoilValue.key}" to have a value`);
}
return loadable.contents;
}
function getPromise<T>(recoilValue: RecoilValue<T>): Promise<T> {
const loadable = getRecoilValueAsLoadable(store, recoilValue);
if (loadable.state !== 'loading') {
throw new Error(`expected atom "${recoilValue.key}" to be a promise`);
}
return loadable.toPromise();
}
let id = 0;
function asyncSelector<T, S>(
dep?: RecoilValue<S>,
): [RecoilValue<T>, (T) => void, (Error) => void, () => boolean] {
let resolve: T => void = () => invariant(false, 'bug in test code'); // make flow happy with initialization
let reject: mixed => void = () => invariant(false, 'bug in test code');
let evaluated = false;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// $FlowFixMe[incompatible-call]
const sel = selector({
key: `AsyncSelector${id++}`,
get: ({get}) => {
evaluated = true;
if (dep != null) {
get(dep);
}
return promise;
},
});
return [sel, resolve, reject, () => evaluated];
}
/* eslint-disable jest/valid-expect */
testRecoil('noWait - resolve', async () => {
const [dep, resolve] = asyncSelector<number, _>();
const pTest = expect(getValue(noWait(dep)).toPromise()).resolves.toBe(42);
expect(getValue(noWait(dep)).contents).toBeInstanceOf(Promise);
resolve(42);
await flushPromisesAndTimers();
expect(getValue(noWait(dep)).contents).toBe(42);
await pTest;
});
testRecoil('noWait - reject', async () => {
const [dep, _resolve, reject] = asyncSelector<$FlowFixMe, _>();
class MyError extends Error {}
const pTest = expect(
getValue(noWait(dep)).toPromise(),
).rejects.toBeInstanceOf(MyError);
expect(getValue(noWait(dep)).contents).toBeInstanceOf(Promise);
reject(new MyError());
await flushPromisesAndTimers();
expect(getValue(noWait(dep)).contents).toBeInstanceOf(MyError);
await pTest;
});
// TRUTH TABLE
// Dependencies waitForNone waitForAny waitForAll waitForAllSettled
// [loading, loading] [Promise, Promise] Promise Promise Promise
// [value, loading] [value, Promise] [value, Promise] Promise Promise
// [value, value] [value, value] [value, value] [value, value] [value, value]
testRecoil('waitFor - resolve to values', async () => {
const [depA, resolveA] = asyncSelector<$FlowFixMe | number, _>();
const [depB, resolveB] = asyncSelector<$FlowFixMe | number, _>();
const deps = [depA, depB];
// Test for initial values
// watiForNone returns loadables with promises that resolve to their values
expect(getValue(waitForNone(deps)).every(r => r.state === 'loading')).toBe(
true,
);
const depTest0 = expect(
getValue(waitForNone(deps))[0].promiseMaybe(),
).resolves.toBe(0);
const depTest1 = expect(
getValue(waitForNone(deps))[1].promiseMaybe(),
).resolves.toBe(1);
// waitForAny returns a promise that resolves to the state with the next
// resolved value. So, that includes the first value and a promise for the second.
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Promise);
const anyTest0 = expect(
getPromise(waitForAny(deps)).then(value => {
expect(value[0].valueMaybe()).toEqual(0);
return value[0].valueMaybe();
}),
).resolves.toEqual(0);
const anyTest1 = expect(
getPromise(waitForAny(deps)).then(value => {
expect(value[1].promiseMaybe()).toBeInstanceOf(Promise);
return value[1].promiseMaybe();
}),
).resolves.toBe(1);
// waitForAll returns a promise that resolves to the actual values
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise);
const allTest0 = expect(getPromise(waitForAll(deps))).resolves.toEqual([
0, 1,
]);
// Resolve the first dep
resolveA(0);
await flushPromisesAndTimers();
expect(getValue(waitForNone(deps))[0].contents).toBe(0);
expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise);
expect(getValue(waitForAny(deps))[0].contents).toBe(0);
expect(getValue(waitForAny(deps))[1].contents).toBeInstanceOf(Promise);
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise);
const allTest1 = expect(getPromise(waitForAll(deps))).resolves.toEqual([
0, 1,
]);
// Resolve the second dep
resolveB(1);
await flushPromisesAndTimers();
expect(getValue(waitForNone(deps))[0].contents).toBe(0);
expect(getValue(waitForNone(deps))[1].contents).toBe(1);
expect(getValue(waitForAny(deps))[0].contents).toBe(0);
expect(getValue(waitForAny(deps))[1].contents).toBe(1);
expect(getValue(waitForAll(deps))[0]).toBe(0);
expect(getValue(waitForAll(deps))[1]).toBe(1);
await depTest0;
await depTest1;
await anyTest0;
await anyTest1;
await allTest0;
await allTest1;
});
// TRUTH TABLE
// Dependencies waitForNone waitForAny waitForAll waitForAllSettled
// [loading, loading] [Promise, Promise] Promise Promise Promise
// [error, loading] [Error, Promise] [Error, Promise] Error Promise
// [error, error] [Error, Error] [Error, Error] Error [Error, Error]
testRecoil('waitFor - rejected', async () => {
const [depA, _resolveA, rejectA] = asyncSelector<$FlowFixMe, _>();
const [depB, _resolveB, rejectB] = asyncSelector<$FlowFixMe, _>();
const deps = [depA, depB];
class Error1 extends Error {}
class Error2 extends Error {}
// All deps Loading Tests
expect(getState(waitForNone(deps))).toEqual('hasValue');
expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array);
expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Promise);
expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise);
expect(getState(waitForAny(deps))).toEqual('loading');
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Promise);
const anyTest0 = expect(
getPromise(waitForAny(deps)).then(res => {
expect(res[0].contents).toBeInstanceOf(Error1);
expect(res[1].contents).toBeInstanceOf(Promise);
return 'success';
}),
).resolves.toEqual('success');
expect(getState(waitForAll(deps))).toEqual('loading');
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise);
const allTest0 = expect(
getPromise(waitForAll(deps)).catch(err => {
expect(err).toBeInstanceOf(Error1);
return 'failure';
}),
).resolves.toEqual('failure');
expect(getState(waitForAllSettled(deps))).toEqual('loading');
expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Promise);
const allSettledTest0 = expect(
getPromise(waitForAllSettled(deps)).then(res => {
expect(res[0].contents).toBeInstanceOf(Error1);
expect(res[1].contents).toBeInstanceOf(Error2);
return 'success';
}),
).resolves.toEqual('success');
// depA Rejected tests
rejectA(new Error1());
await flushPromisesAndTimers();
expect(getState(waitForNone(deps))).toEqual('hasValue');
expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array);
expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise);
expect(getState(waitForAny(deps))).toEqual('hasValue');
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Array);
expect(getValue(waitForAny(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForAny(deps))[1].contents).toBeInstanceOf(Promise);
expect(getState(waitForAll(deps))).toEqual('hasError');
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Error1);
expect(getState(waitForAllSettled(deps))).toEqual('loading');
expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Promise);
const allSettledTest1 = expect(
getPromise(waitForAllSettled(deps)).then(res => {
expect(res[0].contents).toBeInstanceOf(Error1);
expect(res[1].contents).toBeInstanceOf(Error2);
return 'success';
}),
).resolves.toEqual('success');
// depB Rejected tests
rejectB(new Error2());
await flushPromisesAndTimers();
expect(getState(waitForNone(deps))).toEqual('hasValue');
expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array);
expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Error2);
expect(getState(waitForAny(deps))).toEqual('hasValue');
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Array);
expect(getValue(waitForAny(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForAny(deps))[1].contents).toBeInstanceOf(Error2);
expect(getState(waitForAll(deps))).toEqual('hasError');
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Error1);
expect(getState(waitForAllSettled(deps))).toEqual('hasValue');
expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Array);
expect(getValue(waitForAllSettled(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForAllSettled(deps))[1].contents).toBeInstanceOf(Error2);
await anyTest0;
await allTest0;
await allSettledTest0;
await allSettledTest1;
});
// TRUTH TABLE
// Dependencies waitForNone waitForAny waitForAll waitForAllSettled
// [loading, loading] [Promise, Promise] Promise Promise Promise
// [value, loading] [value, Promise] [value, Promise] Promise Promise
// [value, error] [value, Error] [value, Error] Error [value, Error]
testRecoil('waitFor - resolve then reject', async () => {
const [depA, resolveA, _rejectA] = asyncSelector<$FlowFixMe | number, _>();
const [depB, _resolveB, rejectB] = asyncSelector<$FlowFixMe | number, _>();
const deps = [depA, depB];
class Error2 extends Error {}
// All deps Loading Tests
expect(getState(waitForNone(deps))).toEqual('hasValue');
expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array);
expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Promise);
expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise);
expect(getState(waitForAny(deps))).toEqual('loading');
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Promise);
const anyTest0 = expect(
getPromise(waitForAny(deps)).then(res => {
expect(res[0].contents).toEqual(1);
expect(res[1].contents).toBeInstanceOf(Promise);
return 'success';
}),
).resolves.toEqual('success');
expect(getState(waitForAll(deps))).toEqual('loading');
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise);
const allTest0 = expect(
getPromise(waitForAll(deps)).catch(err => {
expect(err).toBeInstanceOf(Error2);
return 'failure';
}),
).resolves.toEqual('failure');
expect(getState(waitForAllSettled(deps))).toEqual('loading');
expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Promise);
const allSettledTest0 = expect(
getPromise(waitForAllSettled(deps)).then(res => {
expect(res[0].contents).toEqual(1);
expect(res[1].contents).toBeInstanceOf(Error2);
return 'success';
}),
).resolves.toEqual('success');
// depA Resolves tests
resolveA(1);
await flushPromisesAndTimers();
expect(getState(waitForNone(deps))).toEqual('hasValue');
expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array);
expect(getValue(waitForNone(deps))[0].contents).toEqual(1);
expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise);
expect(getState(waitForAny(deps))).toEqual('hasValue');
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Array);
expect(getValue(waitForAny(deps))[0].contents).toEqual(1);
expect(getValue(waitForAny(deps))[1].contents).toBeInstanceOf(Promise);
expect(getState(waitForAll(deps))).toEqual('loading');
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise);
const allTest1 = expect(getPromise(waitForAll(deps))).rejects.toBeInstanceOf(
Error2,
);
expect(getState(waitForAllSettled(deps))).toEqual('loading');
expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Promise);
const allSettledTest1 = expect(
getPromise(waitForAllSettled(deps)).then(res => {
expect(res[0].contents).toEqual(1);
expect(res[1].contents).toBeInstanceOf(Error2);
return 'success';
}),
).resolves.toEqual('success');
// depB Rejected tests
rejectB(new Error2());
await flushPromisesAndTimers();
expect(getState(waitForNone(deps))).toEqual('hasValue');
expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array);
expect(getValue(waitForNone(deps))[0].contents).toEqual(1);
expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Error2);
expect(getState(waitForAny(deps))).toEqual('hasValue');
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Array);
expect(getValue(waitForAny(deps))[0].contents).toEqual(1);
expect(getValue(waitForAny(deps))[1].contents).toBeInstanceOf(Error2);
expect(getState(waitForAll(deps))).toEqual('hasError');
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Error2);
expect(getState(waitForAllSettled(deps))).toEqual('hasValue');
expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Array);
expect(getValue(waitForAllSettled(deps))[0].contents).toEqual(1);
expect(getValue(waitForAllSettled(deps))[1].contents).toBeInstanceOf(Error2);
await anyTest0;
await allTest0;
await allTest1;
await allSettledTest0;
await allSettledTest1;
});
// TRUTH TABLE
// Dependencies waitForNone waitForAny waitForAll waitForAllSettled
// [loading, loading] [Promise, Promise] Promise Promise Promise
// [error, loading] [Error, Promise] [Error, Promsie] Error Promise
// [error, value] [Error, value] [Error, value] Error [Error, value]
testRecoil('waitFor - reject then resolve', async () => {
const [depA, _resolveA, rejectA] = asyncSelector<$FlowFixMe | number, _>();
const [depB, resolveB, _rejectB] = asyncSelector<$FlowFixMe | number, _>();
const deps = [depA, depB];
class Error1 extends Error {}
// All deps Loading Tests
expect(getState(waitForNone(deps))).toEqual('hasValue');
expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array);
expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Promise);
expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise);
expect(getState(waitForAny(deps))).toEqual('loading');
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Promise);
const anyTest0 = expect(
getPromise(waitForAny(deps)).then(res => {
expect(res[0].contents).toBeInstanceOf(Error1);
expect(res[1].contents).toBeInstanceOf(Promise);
return 'success';
}),
).resolves.toEqual('success');
expect(getState(waitForAll(deps))).toEqual('loading');
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise);
const allTest0 = expect(
getPromise(waitForAll(deps)).catch(err => {
expect(err).toBeInstanceOf(Error1);
return 'failure';
}),
).resolves.toEqual('failure');
expect(getState(waitForAllSettled(deps))).toEqual('loading');
expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Promise);
const allSettledTest0 = expect(
getPromise(waitForAllSettled(deps)).then(res => {
expect(res[0].contents).toBeInstanceOf(Error1);
expect(res[1].contents).toEqual(1);
return 'success';
}),
).resolves.toEqual('success');
// depA Rejects tests
rejectA(new Error1());
await flushPromisesAndTimers();
expect(getState(waitForNone(deps))).toEqual('hasValue');
expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array);
expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise);
expect(getState(waitForAny(deps))).toEqual('hasValue');
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Array);
expect(getValue(waitForAny(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForAny(deps))[1].contents).toBeInstanceOf(Promise);
expect(getState(waitForAll(deps))).toEqual('hasError');
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Error1);
expect(getState(waitForAllSettled(deps))).toEqual('loading');
expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Promise);
const allSettledTest1 = expect(
getPromise(waitForAllSettled(deps)).then(res => {
expect(res[0].contents).toBeInstanceOf(Error1);
expect(res[1].contents).toEqual(1);
return 'success';
}),
).resolves.toEqual('success');
// depB Resolves tests
resolveB(1);
await flushPromisesAndTimers();
expect(getState(waitForNone(deps))).toEqual('hasValue');
expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array);
expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForNone(deps))[1].contents).toEqual(1);
expect(getState(waitForAny(deps))).toEqual('hasValue');
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Array);
expect(getValue(waitForAny(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForAny(deps))[1].contents).toEqual(1);
expect(getState(waitForAll(deps))).toEqual('hasError');
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Error1);
expect(getState(waitForAllSettled(deps))).toEqual('hasValue');
expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Array);
expect(getValue(waitForAllSettled(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForAllSettled(deps))[1].contents).toEqual(1);
await anyTest0;
await allTest0;
await allSettledTest0;
await allSettledTest1;
});
// Similar as the first test that resolves both dependencies, but with named dependencies.
testRecoil('waitFor - named dependency version', async () => {
const [depA, resolveA] = asyncSelector<$FlowFixMe | number, _>();
const [depB, resolveB] = asyncSelector<$FlowFixMe | number, _>();
const deps = {a: depA, b: depB};
expect(getValue(waitForNone(deps)).a.promiseMaybe()).toBeInstanceOf(Promise);
expect(getValue(waitForNone(deps)).b.promiseMaybe()).toBeInstanceOf(Promise);
const depTest0 = expect(
getValue(waitForNone(deps)).a.promiseMaybe(),
).resolves.toBe(0);
const depTest1 = expect(
getValue(waitForNone(deps)).b.promiseMaybe(),
).resolves.toBe(1);
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Promise);
const anyTest0 = expect(
getPromise(waitForAny(deps)).then(value => {
expect(value.a.valueMaybe()).toEqual(0);
return value.a.valueMaybe();
}),
).resolves.toEqual(0);
const anyTest1 = expect(
getPromise(waitForAny(deps)).then(value => {
expect(value.b.promiseMaybe()).toBeInstanceOf(Promise);
return value.b.promiseMaybe();
}),
).resolves.toBe(1);
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise);
const allTest0 = expect(getPromise(waitForAll(deps))).resolves.toEqual({
a: 0,
b: 1,
});
resolveA(0);
await flushPromisesAndTimers();
expect(getValue(waitForNone(deps)).a.contents).toBe(0);
expect(getValue(waitForNone(deps)).b.contents).toBeInstanceOf(Promise);
expect(getValue(waitForAny(deps)).a.contents).toBe(0);
expect(getValue(waitForAny(deps)).b.contents).toBeInstanceOf(Promise);
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise);
const allTest1 = expect(getPromise(waitForAll(deps))).resolves.toEqual({
a: 0,
b: 1,
});
resolveB(1);
await flushPromisesAndTimers();
expect(getValue(waitForNone(deps)).a.contents).toBe(0);
expect(getValue(waitForNone(deps)).b.contents).toBe(1);
expect(getValue(waitForAny(deps)).a.contents).toBe(0);
expect(getValue(waitForAny(deps)).b.contents).toBe(1);
expect(getValue(waitForAll(deps)).a).toBe(0);
expect(getValue(waitForAll(deps)).b).toBe(1);
await depTest0;
await depTest1;
await anyTest0;
await anyTest1;
await allTest0;
await allTest1;
});
testRecoil('waitForAll - Evaluated concurrently', async () => {
const [depA, resolveA, _rejectA, evaluatedA] = asyncSelector<
$FlowFixMe | number,
_,
>();
const [depB, _resolveB, _rejectB, evaluatedB] = asyncSelector<
$FlowFixMe,
_,
>();
const deps = [depA, depB];
expect(evaluatedA()).toBe(false);
expect(evaluatedB()).toBe(false);
getPromise(waitForAll(deps));
await flushPromisesAndTimers();
// Confirm dependencies were evaluated in parallel
expect(evaluatedA()).toBe(true);
expect(evaluatedB()).toBe(true);
resolveA(0);
getPromise(waitForAll(deps));
await flushPromisesAndTimers();
expect(evaluatedA()).toBe(true);
expect(evaluatedB()).toBe(true);
});
testRecoil('waitForAll - mixed sync and async deps', async () => {
const [depA, resolveA] = asyncSelector<$FlowFixMe | number, _>();
const depB = selector({
key: 'mydepkeyB',
get: () => 1,
});
const deps = [depA, depB];
const allTest = expect(getPromise(waitForAll(deps))).resolves.toEqual([0, 1]);
resolveA(0);
await flushPromisesAndTimers();
expect(getValue(waitForAll(deps))).toEqual([0, 1]);
await allTest;
});
/* eslint-enable jest/valid-expect */

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,762 @@
/**
* 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 {Store} from '../../core/Recoil_State';
import type {Parameter} from 'Recoil_atomFamily';
import type {NodeKey, StoreID as StoreIDType} from 'Recoil_Keys';
import type {RecoilState} from 'Recoil_RecoilValue';
import type {Node} from 'react';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let store: Store,
React,
Profiler,
useState,
act,
RecoilRoot,
getRecoilValueAsLoadable,
setRecoilValue,
useRecoilState,
useRecoilValue,
useSetRecoilState,
useSetUnvalidatedAtomValues,
useRecoilStoreID,
ReadsAtom,
componentThatReadsAndWritesAtom,
flushPromisesAndTimers,
renderElements,
reactMode,
stableStringify,
atom,
atomFamily,
selectorFamily,
RecoilLoadable,
pAtom;
const testRecoil = getRecoilTestFn(() => {
const {
makeStore,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
React = require('react');
({Profiler, useState} = require('react'));
({act} = require('ReactTestUtils'));
({RecoilRoot, useRecoilStoreID} = require('../../core/Recoil_RecoilRoot'));
({
getRecoilValueAsLoadable,
setRecoilValue,
} = require('../../core/Recoil_RecoilValueInterface'));
({
useRecoilState,
useRecoilValue,
useSetRecoilState,
useSetUnvalidatedAtomValues,
} = require('../../hooks/Recoil_Hooks'));
({
ReadsAtom,
componentThatReadsAndWritesAtom,
flushPromisesAndTimers,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({reactMode} = require('../../core/Recoil_ReactMode'));
stableStringify = require('recoil-shared/util/Recoil_stableStringify');
atom = require('../Recoil_atom');
atomFamily = require('../Recoil_atomFamily');
selectorFamily = require('../Recoil_selectorFamily');
({RecoilLoadable} = require('../../adt/Recoil_Loadable'));
store = makeStore();
pAtom = atomFamily<_, {k: string} | {x: string} | {y: string}>({
key: 'pAtom',
default: 'fallback',
});
});
let fbOnlyTest = test.skip;
// $FlowFixMe[prop-missing]
// $FlowFixMe[incompatible-type]
// @fb-only: fbOnlyTest = testRecoil;
let id = 0;
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
function get(recoilValue) {
return getRecoilValueAsLoadable(store, recoilValue).contents;
}
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
function getLoadable(recoilValue) {
return getRecoilValueAsLoadable(store, recoilValue);
}
function set(
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
recoilValue,
value:
| void
| number
| $TEMPORARY$string<'VALUE'>
| $TEMPORARY$string<'bar'>
| $TEMPORARY$string<'eggs'>
| $TEMPORARY$string<'spam'>
| $TEMPORARY$string<'xValue'>
| $TEMPORARY$string<'xValue1'>
| $TEMPORARY$string<'xValue2'>
| $TEMPORARY$string<'xValue3'>
| $TEMPORARY$string<'xValue4'>
| $TEMPORARY$string<'yValue'>
| $TEMPORARY$string<'yValue1'>
| $TEMPORARY$string<'yValue2'>
| $TEMPORARY$string<'yValue3'>
| $TEMPORARY$string<'yValue4'>,
) {
setRecoilValue(store, recoilValue, value);
}
testRecoil('Read fallback by default', () => {
expect(get(pAtom({k: 'x'}))).toBe('fallback');
});
testRecoil('Uses value for parameter', () => {
set(pAtom({k: 'x'}), 'xValue');
set(pAtom({k: 'y'}), 'yValue');
expect(get(pAtom({k: 'x'}))).toBe('xValue');
expect(get(pAtom({k: 'y'}))).toBe('yValue');
expect(get(pAtom({k: 'z'}))).toBe('fallback');
});
testRecoil('Works with non-overlapping sets', () => {
set(pAtom({x: 'x'}), 'xValue');
set(pAtom({y: 'y'}), 'yValue');
expect(get(pAtom({x: 'x'}))).toBe('xValue');
expect(get(pAtom({y: 'y'}))).toBe('yValue');
});
describe('Default', () => {
testRecoil('default is optional', () => {
const myAtom = atom<$FlowFixMe>({key: 'atom without default'});
expect(getLoadable(myAtom).state).toBe('loading');
act(() => set(myAtom, 'VALUE'));
expect(get(myAtom)).toBe('VALUE');
});
testRecoil('Works with atom default', () => {
const fallbackAtom = atom({key: 'fallback', default: 0});
const hasFallback = atomFamily<_, {k: string}>({
key: 'hasFallback',
default: fallbackAtom,
});
expect(get(hasFallback({k: 'x'}))).toBe(0);
set(fallbackAtom, 1);
expect(get(hasFallback({k: 'x'}))).toBe(1);
set(hasFallback({k: 'x'}), 2);
expect(get(hasFallback({k: 'x'}))).toBe(2);
expect(get(hasFallback({k: 'y'}))).toBe(1);
});
testRecoil('Works with parameterized default', () => {
const paramDefaultAtom = atomFamily<_, {num: number}>({
key: 'parameterized default',
// $FlowFixMe[missing-local-annot]
default: ({num}) => num,
});
expect(get(paramDefaultAtom({num: 1}))).toBe(1);
expect(get(paramDefaultAtom({num: 2}))).toBe(2);
set(paramDefaultAtom({num: 1}), 3);
expect(get(paramDefaultAtom({num: 1}))).toBe(3);
expect(get(paramDefaultAtom({num: 2}))).toBe(2);
});
testRecoil('Parameterized async default', async () => {
const paramDefaultAtom = atomFamily<_, {num: number}>({
key: 'parameterized async default',
// $FlowFixMe[missing-local-annot]
default: ({num}) =>
num === 1 ? Promise.reject(num) : Promise.resolve(num),
});
await expect(get(paramDefaultAtom({num: 1}))).rejects.toBe(1);
await expect(get(paramDefaultAtom({num: 2}))).resolves.toBe(2);
set(paramDefaultAtom({num: 1}), 3);
expect(get(paramDefaultAtom({num: 1}))).toBe(3);
expect(get(paramDefaultAtom({num: 2}))).toBe(2);
});
testRecoil('Parameterized loadable default', async () => {
const paramDefaultAtom = atomFamily<_, {num: number}>({
key: 'parameterized loadable default',
// $FlowFixMe[missing-local-annot]
default: ({num}) =>
// $FlowFixMe[underconstrained-implicit-instantiation]
num === 1 ? RecoilLoadable.error(num) : RecoilLoadable.of(num),
});
expect(getLoadable(paramDefaultAtom({num: 1})).state).toBe('hasError');
expect(getLoadable(paramDefaultAtom({num: 1})).contents).toBe(1);
expect(getLoadable(paramDefaultAtom({num: 2})).state).toBe('hasValue');
expect(getLoadable(paramDefaultAtom({num: 2})).contents).toBe(2);
set(paramDefaultAtom({num: 1}), 3);
expect(getLoadable(paramDefaultAtom({num: 1})).state).toBe('hasValue');
expect(getLoadable(paramDefaultAtom({num: 1})).contents).toBe(3);
expect(getLoadable(paramDefaultAtom({num: 2})).state).toBe('hasValue');
expect(getLoadable(paramDefaultAtom({num: 2})).contents).toBe(2);
});
});
testRecoil('Works with date as parameter', () => {
const dateAtomFamily = atomFamily<_, Date>({
key: 'dateFamily',
// $FlowFixMe[missing-local-annot]
default: _date => 0,
});
expect(get(dateAtomFamily(new Date(2021, 2, 25)))).toBe(0);
expect(get(dateAtomFamily(new Date(2021, 2, 26)))).toBe(0);
set(dateAtomFamily(new Date(2021, 2, 25)), 1);
expect(get(dateAtomFamily(new Date(2021, 2, 25)))).toBe(1);
expect(get(dateAtomFamily(new Date(2021, 2, 26)))).toBe(0);
});
testRecoil('Works with parameterized fallback', () => {
const fallbackAtom = atomFamily<_, $FlowFixMe | {num: number}>({
key: 'parameterized fallback default',
// $FlowFixMe[missing-local-annot]
default: ({num}) => num * 10,
});
const paramFallbackAtom = atomFamily<_, {num: number}>({
key: 'parameterized fallback',
default: fallbackAtom,
});
expect(get(paramFallbackAtom({num: 1}))).toBe(10);
expect(get(paramFallbackAtom({num: 2}))).toBe(20);
set(paramFallbackAtom({num: 1}), 3);
expect(get(paramFallbackAtom({num: 1}))).toBe(3);
expect(get(paramFallbackAtom({num: 2}))).toBe(20);
set(fallbackAtom({num: 2}), 200);
expect(get(paramFallbackAtom({num: 2}))).toBe(200);
set(fallbackAtom({num: 1}), 100);
expect(get(paramFallbackAtom({num: 1}))).toBe(3);
expect(get(paramFallbackAtom({num: 2}))).toBe(200);
});
testRecoil('atomFamily async fallback', async () => {
const paramFallback = atomFamily<_, {}>({
key: 'paramaterizedAtom async Fallback',
default: Promise.resolve(42),
});
const container = renderElements(<ReadsAtom atom={paramFallback({})} />);
expect(container.textContent).toEqual('loading');
act(() => jest.runAllTimers());
await flushPromisesAndTimers();
expect(container.textContent).toEqual('42');
});
testRecoil('Parameterized fallback with atom and async', async () => {
const paramFallback = atomFamily<_, {param: string}>({
key: 'parameterized async Fallback',
// $FlowFixMe[missing-local-annot]
default: ({param}) =>
({
value: 'value',
atom: atom({key: `param async fallback atom ${id++}`, default: 'atom'}),
async: Promise.resolve('async'),
}[param]),
});
const valueCont = renderElements(
<ReadsAtom atom={paramFallback({param: 'value'})} />,
);
expect(valueCont.textContent).toEqual('"value"');
const atomCont = renderElements(
<ReadsAtom atom={paramFallback({param: 'atom'})} />,
);
expect(atomCont.textContent).toEqual('"atom"');
const asyncCont = renderElements(
<ReadsAtom atom={paramFallback({param: 'async'})} />,
);
expect(asyncCont.textContent).toEqual('loading');
act(() => jest.runAllTimers());
await flushPromisesAndTimers();
expect(asyncCont.textContent).toEqual('"async"');
});
fbOnlyTest('atomFamily with scope', () => {
const scopeForParamAtom = atom<string>({
key: 'scope atom for atomFamily',
default: 'foo',
});
const paramAtomWithScope = atomFamily<string, {k: string}>({
key: 'parameterized atom with scope',
default: 'default',
scopeRules_APPEND_ONLY_READ_THE_DOCS: [[scopeForParamAtom]],
});
expect(get(paramAtomWithScope({k: 'x'}))).toBe('default');
expect(get(paramAtomWithScope({k: 'y'}))).toBe('default');
set(paramAtomWithScope({k: 'x'}), 'xValue1');
expect(get(paramAtomWithScope({k: 'x'}))).toBe('xValue1');
expect(get(paramAtomWithScope({k: 'y'}))).toBe('default');
set(paramAtomWithScope({k: 'y'}), 'yValue1');
expect(get(paramAtomWithScope({k: 'x'}))).toBe('xValue1');
expect(get(paramAtomWithScope({k: 'y'}))).toBe('yValue1');
set(scopeForParamAtom, 'bar');
expect(get(paramAtomWithScope({k: 'x'}))).toBe('default');
expect(get(paramAtomWithScope({k: 'y'}))).toBe('default');
set(paramAtomWithScope({k: 'x'}), 'xValue2');
expect(get(paramAtomWithScope({k: 'x'}))).toBe('xValue2');
expect(get(paramAtomWithScope({k: 'y'}))).toBe('default');
set(paramAtomWithScope({k: 'y'}), 'yValue2');
expect(get(paramAtomWithScope({k: 'x'}))).toBe('xValue2');
expect(get(paramAtomWithScope({k: 'y'}))).toBe('yValue2');
});
fbOnlyTest('atomFamily with parameterized scope', () => {
const paramScopeForParamAtom = atomFamily<string, {namespace: string}>({
key: 'scope atom for atomFamily with parameterized scope',
default: ({namespace}) => namespace,
});
const paramAtomWithParamScope = atomFamily<string, {k: string, n: string}>({
key: 'parameterized atom with parameterized scope',
default: 'default',
scopeRules_APPEND_ONLY_READ_THE_DOCS: [
[({n}) => paramScopeForParamAtom({namespace: n})],
],
});
expect(get(paramScopeForParamAtom({namespace: 'foo'}))).toBe('foo');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('default');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('default');
set(paramAtomWithParamScope({n: 'foo', k: 'x'}), 'xValue1');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('xValue1');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('default');
set(paramAtomWithParamScope({n: 'foo', k: 'y'}), 'yValue1');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('xValue1');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('yValue1');
set(paramScopeForParamAtom({namespace: 'foo'}), 'eggs');
expect(get(paramScopeForParamAtom({namespace: 'foo'}))).toBe('eggs');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('default');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('default');
set(paramAtomWithParamScope({n: 'foo', k: 'x'}), 'xValue2');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('xValue2');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('default');
set(paramAtomWithParamScope({n: 'foo', k: 'y'}), 'yValue2');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('xValue2');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('yValue2');
expect(get(paramScopeForParamAtom({namespace: 'bar'}))).toBe('bar');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('default');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('default');
set(paramAtomWithParamScope({n: 'bar', k: 'x'}), 'xValue3');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('xValue3');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('default');
set(paramAtomWithParamScope({n: 'bar', k: 'y'}), 'yValue3');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('xValue3');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('yValue3');
set(paramScopeForParamAtom({namespace: 'bar'}), 'spam');
expect(get(paramScopeForParamAtom({namespace: 'bar'}))).toBe('spam');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('default');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('default');
set(paramAtomWithParamScope({n: 'bar', k: 'x'}), 'xValue4');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('xValue4');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('default');
set(paramAtomWithParamScope({n: 'bar', k: 'y'}), 'yValue4');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('xValue4');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('yValue4');
});
testRecoil('Returns the fallback for parameterized atoms', () => {
let theAtom = null;
let setUnvalidatedAtomValues;
let setAtomParam;
let setAtomValue;
function SetsUnvalidatedAtomValues() {
setUnvalidatedAtomValues = useSetUnvalidatedAtomValues();
return null;
}
let setVisible;
function Switch({children}: $TEMPORARY$object<{children: Node}>) {
const [visible, mySetVisible] = useState(false);
setVisible = mySetVisible;
return visible ? children : null;
}
function MyReadsAtom({
getAtom,
}: $TEMPORARY$object<{
getAtom: () => null | (Parameter => RecoilState<number>),
}>) {
const [param, setParam] = useState({num: 1});
setAtomParam = setParam;
// flowlint-next-line unclear-type:off
const myAtom: any = getAtom();
const [value, setValue] = useRecoilState(myAtom(param));
setAtomValue = setValue;
return value;
}
const container = renderElements(
<>
<SetsUnvalidatedAtomValues />
<Switch>
<MyReadsAtom getAtom={() => theAtom} />
</Switch>
</>,
);
act(() => {
setUnvalidatedAtomValues(
new Map<NodeKey, mixed>().set('notDefinedYetAtomFamilyWithFallback', 123),
);
});
const fallback = atom<number>({
key: 'fallback for atomFamily',
default: 222,
});
theAtom = atomFamily<_, Parameter>({
key: 'notDefinedYetAtomFamilyWithFallback',
default: fallback,
persistence_UNSTABLE: {
type: 'url',
validator: (_, returnFallback) => returnFallback,
},
});
act(() => {
setVisible(true);
});
expect(container.textContent).toBe('222');
act(() => {
setAtomValue(111);
});
expect(container.textContent).toBe('111');
act(() => {
setAtomParam({num: 2});
});
expect(container.textContent).toBe('222');
});
testRecoil(
'Returns the fallback for parameterized atoms with a selector as the fallback',
() => {
let theAtom = null;
let setUnvalidatedAtomValues;
let setAtomParam;
let setAtomValue;
function SetsUnvalidatedAtomValues() {
setUnvalidatedAtomValues = useSetUnvalidatedAtomValues();
return null;
}
let setVisible;
function Switch({children}: $TEMPORARY$object<{children: Node}>) {
const [visible, mySetVisible] = useState(false);
setVisible = mySetVisible;
return visible ? children : null;
}
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by
* Flow's LTI update could not be added via codemod */
function MyReadsAtom({getAtom}) {
const [param, setParam] = useState({num: 10});
setAtomParam = setParam;
// flowlint-next-line unclear-type:off
const myAtom: any = getAtom();
const [value, setValue] = useRecoilState(myAtom(param));
setAtomValue = setValue;
return value;
}
const container = renderElements(
<>
<SetsUnvalidatedAtomValues />
<Switch>
<MyReadsAtom getAtom={() => theAtom} />
</Switch>
</>,
);
act(() => {
setUnvalidatedAtomValues(
new Map<NodeKey, mixed>().set(
'notDefinedYetAtomFamilyFallbackSel',
123,
),
);
});
theAtom = atomFamily<_, $FlowFixMe>({
key: 'notDefinedYetAtomFamilyFallbackSel',
default: selectorFamily({
key: 'notDefinedYetAtomFamilyFallbackSelFallback',
get:
(
// $FlowFixMe[missing-local-annot]
{num},
) =>
() =>
num === 1 ? 456 : 789,
}),
persistence_UNSTABLE: {
type: 'url',
validator: (_, notValid) => notValid,
},
});
act(() => {
setVisible(true);
});
expect(container.textContent).toBe('789');
act(() => {
setAtomValue(111);
});
expect(container.textContent).toBe('111');
act(() => {
setAtomParam({num: 1});
});
expect(container.textContent).toBe('456');
},
);
testRecoil('Independent atom subscriptions', ({gks}) => {
const BASE_CALLS =
reactMode().mode === 'LEGACY' &&
!gks.includes('recoil_suppress_rerender_in_callback')
? 1
: 0;
const myAtom = atomFamily<_, string>({
key: 'atomFamily/independent subscriptions',
default: 'DEFAULT',
});
const TrackingComponent = (
param: $TEMPORARY$string<'A'> | $TEMPORARY$string<'B'>,
) => {
let numUpdates = 0;
let setValue;
const Component = () => {
setValue = useSetRecoilState(myAtom(param));
return (
<Profiler
id="test"
onRender={() => {
numUpdates++;
}}>
{stableStringify(useRecoilValue(myAtom(param)))}
</Profiler>
);
};
// $FlowFixMe[incompatible-call]
return [Component, (value: number) => setValue(value), () => numUpdates];
};
const [ComponentA, setValueA, getNumUpdatesA] = TrackingComponent('A');
const [ComponentB, setValueB, getNumUpdatesB] = TrackingComponent('B');
const container = renderElements(
<>
<ComponentA />
<ComponentB />
</>,
);
// Initial:
expect(container.textContent).toBe('"DEFAULT""DEFAULT"');
expect(getNumUpdatesA()).toBe(BASE_CALLS + 1);
expect(getNumUpdatesB()).toBe(BASE_CALLS + 1);
// After setting at parameter A, component A should update:
act(() => setValueA(1));
expect(container.textContent).toBe('1"DEFAULT"');
expect(getNumUpdatesA()).toBe(BASE_CALLS + 2);
expect(getNumUpdatesB()).toBe(BASE_CALLS + 1);
// After setting at parameter B, component B should update:
act(() => setValueB(2));
expect(container.textContent).toBe('12');
expect(getNumUpdatesA()).toBe(BASE_CALLS + 2);
expect(getNumUpdatesB()).toBe(BASE_CALLS + 2);
});
describe('Effects', () => {
testRecoil('Initialization', () => {
let inited = 0;
const myFamily = atomFamily<string, number>({
key: 'atomFamily effect init',
default: 'DEFAULT',
effects: [
({setSelf}) => {
inited++;
setSelf('INIT');
},
],
});
expect(inited).toEqual(0);
expect(get(myFamily(1))).toEqual('INIT');
expect(inited).toEqual(1);
set(myFamily(2));
expect(inited).toEqual(2);
const [ReadsWritesAtom, _, reset] = componentThatReadsAndWritesAtom(
myFamily(1),
);
const c = renderElements(<ReadsWritesAtom />);
expect(c.textContent).toEqual('"INIT"');
act(reset);
expect(c.textContent).toEqual('"DEFAULT"');
});
testRecoil('Parameterized Initialization', () => {
const myFamily = atomFamily({
key: 'atomFamily effect parameterized init',
default: 'DEFAULT',
// $FlowFixMe[missing-local-annot]
effects: param => [({setSelf}) => setSelf(param)],
});
expect(get(myFamily(1))).toEqual(1);
expect(get(myFamily(2))).toEqual(2);
});
testRecoil('Cleanup Handlers - when root unmounted', () => {
const refCounts: {[string]: number} = {A: 0, B: 0};
const atoms = atomFamily({
key: 'atomFamily effect cleanup',
// $FlowFixMe[missing-local-annot]
default: p => p,
// $FlowFixMe[missing-local-annot]
effects: p => [
() => {
refCounts[p]++;
return () => {
refCounts[p]--;
};
},
],
});
let setNumRoots;
function App() {
const [numRoots, _setNumRoots] = useState(0);
setNumRoots = _setNumRoots;
return (
<div>
{Array(numRoots)
.fill(null)
.map((_, idx) => (
<RecoilRoot key={idx}>
<ReadsAtom atom={atoms('A')} />
<ReadsAtom atom={atoms('B')} />
</RecoilRoot>
))}
</div>
);
}
const c = renderElements(<App />);
expect(c.textContent).toBe('');
expect(refCounts).toEqual({A: 0, B: 0});
act(() => setNumRoots(1));
expect(c.textContent).toBe('"A""B"');
expect(refCounts).toEqual({A: 1, B: 1});
act(() => setNumRoots(2));
expect(c.textContent).toBe('"A""B""A""B"');
expect(refCounts).toEqual({A: 2, B: 2});
act(() => setNumRoots(1));
expect(c.textContent).toBe('"A""B"');
expect(refCounts).toEqual({A: 1, B: 1});
act(() => setNumRoots(0));
expect(c.textContent).toBe('');
expect(refCounts).toEqual({A: 0, B: 0});
});
testRecoil('storeID matches <RecoilRoot>', async () => {
const atoms = atomFamily({
key: 'atomFamily effect - storeID',
default: 'DEFAULT',
// $FlowFixMe[missing-local-annot]
effects: rootKey => [
({storeID, setSelf}) => {
expect(storeID).toEqual(storeIDs[rootKey]);
setSelf(rootKey);
},
],
});
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" />
<ReadsAtom atom={atoms('A')} />
<RecoilRoot>
<StoreID rootKey="A1" />
<ReadsAtom atom={atoms('A1')} />
</RecoilRoot>
<RecoilRoot override={false}>
<StoreID rootKey="A2" />
<ReadsAtom atom={atoms('A2')} />
</RecoilRoot>
</RecoilRoot>
<RecoilRoot>
<StoreID rootKey="B" />
<ReadsAtom atom={atoms('B')} />
</RecoilRoot>
</div>
);
}
const c = renderElements(<MyApp />);
expect(c.textContent).toEqual('"A""A1""A2""B"');
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);
});
});

View File

@@ -0,0 +1,260 @@
/**
* 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 {RecoilValue} from '../../core/Recoil_RecoilValue';
import type {Store} from '../../core/Recoil_State';
import type {NodeKey} from 'Recoil_Keys';
import type {RecoilState} from 'Recoil_RecoilValue';
import type {Node} from 'react';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useState,
act,
getRecoilValueAsLoadable,
setRecoilValue,
subscribeToRecoilValue,
useRecoilState,
useSetUnvalidatedAtomValues,
componentThatReadsAndWritesAtom,
renderElements,
atom,
constSelector,
store: Store;
let fallbackAtom: RecoilValue<number>, hasFallbackAtom: RecoilValue<number>;
let id = 0;
const testRecoil = getRecoilTestFn(() => {
const {
makeStore,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
React = require('react');
({useState} = require('react'));
({act} = require('ReactTestUtils'));
({
getRecoilValueAsLoadable,
setRecoilValue,
subscribeToRecoilValue,
} = require('../../core/Recoil_RecoilValueInterface'));
({
useRecoilState,
useSetUnvalidatedAtomValues,
} = require('../../hooks/Recoil_Hooks'));
({
componentThatReadsAndWritesAtom,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
atom = require('../Recoil_atom');
constSelector = require('../Recoil_constSelector');
store = makeStore();
fallbackAtom = atom<number>({key: `fallback${id}`, default: 1});
hasFallbackAtom = atom<number>({
key: `hasFallback${id++}`,
default: fallbackAtom,
});
subscribeToRecoilValue(store, hasFallbackAtom, () => undefined);
});
function get(
recoilValue: RecoilState<string> | RecoilState<?string> | RecoilValue<number>,
) {
return getRecoilValueAsLoadable(store, recoilValue).contents;
}
function set(
recoilValue: RecoilState<?string> | RecoilValue<number>,
value: ?(number | $TEMPORARY$string<'VALUE'>),
) {
setRecoilValue(store, recoilValue, value);
}
testRecoil('atomWithFallback', () => {
expect(get(hasFallbackAtom)).toBe(1);
set(fallbackAtom, 2);
expect(get(hasFallbackAtom)).toBe(2);
set(hasFallbackAtom, 3);
expect(get(hasFallbackAtom)).toBe(3);
});
describe('ReturnDefaultOrFallback', () => {
testRecoil('Returns the default', () => {
let theAtom = null;
let setUnvalidatedAtomValues;
function SetsUnvalidatedAtomValues() {
setUnvalidatedAtomValues = useSetUnvalidatedAtomValues();
return null;
}
let setVisible;
function Switch({children}: $TEMPORARY$object<{children: Node}>) {
const [visible, mySetVisible] = useState(false);
setVisible = mySetVisible;
return visible ? children : null;
}
function MyReadsAtom({
getAtom,
}: $TEMPORARY$object<{getAtom: () => null | RecoilState<number>}>) {
// flowlint-next-line unclear-type:off
const [value] = useRecoilState((getAtom(): any));
return value;
}
const container = renderElements(
<>
<SetsUnvalidatedAtomValues />
<Switch>
<MyReadsAtom getAtom={() => theAtom} />
</Switch>
</>,
);
act(() => {
setUnvalidatedAtomValues(
new Map<NodeKey, mixed>().set('notDefinedYetAtomValidator', 123),
);
});
theAtom = atom({
key: 'notDefinedYetAtomValidator',
default: 456,
persistence_UNSTABLE: {
type: 'url',
validator: (_, returnFallback) => returnFallback,
},
});
act(() => {
setVisible(true);
});
expect(container.textContent).toBe('456');
});
testRecoil('Returns the fallback', () => {
let theAtom = null;
let setUnvalidatedAtomValues;
function SetsUnvalidatedAtomValues() {
setUnvalidatedAtomValues = useSetUnvalidatedAtomValues();
return null;
}
let setVisible;
function Switch({children}: $TEMPORARY$object<{children: Node}>) {
const [visible, mySetVisible] = useState(false);
setVisible = mySetVisible;
return visible ? children : null;
}
function MyReadsAtom({
getAtom,
}: $TEMPORARY$object<{getAtom: () => null | RecoilState<number>}>) {
// flowlint-next-line unclear-type:off
const [value] = useRecoilState((getAtom(): any));
return value;
}
const container = renderElements(
<>
<SetsUnvalidatedAtomValues />
<Switch>
<MyReadsAtom getAtom={() => theAtom} />
</Switch>
</>,
);
act(() => {
setUnvalidatedAtomValues(
new Map<NodeKey, mixed>().set('notDefinedYetAtomWithFallback', 123),
);
});
const fallback = atom<number>({
key: 'notDefinedYetAtomFallback',
default: 222,
});
theAtom = atom({
key: 'notDefinedYetAtomWithFallback',
default: fallback,
persistence_UNSTABLE: {
type: 'url',
validator: (_, returnFallback) => returnFallback,
},
});
act(() => {
setVisible(true);
});
expect(container.textContent).toBe('222');
});
});
testRecoil('Atom with atom fallback can store null and undefined', () => {
const myFallbackAtom = atom<?string>({
key: 'fallback for null undefined',
default: 'FALLBACK',
});
const myAtom = atom<?string>({
key: 'fallback atom with undefined',
default: myFallbackAtom,
});
expect(get(myAtom)).toBe('FALLBACK');
act(() => set(myAtom, 'VALUE'));
expect(get(myAtom)).toBe('VALUE');
act(() => set(myAtom, null));
expect(get(myAtom)).toBe(null);
act(() => set(myAtom, undefined));
expect(get(myAtom)).toBe(undefined);
act(() => set(myAtom, 'VALUE'));
expect(get(myAtom)).toBe('VALUE');
});
testRecoil('Atom with selector fallback can store null and undefined', () => {
const fallbackSelector = constSelector('FALLBACK');
const myAtom = atom<?string>({
key: 'fallback selector with undefined',
default: fallbackSelector,
});
expect(get(myAtom)).toBe('FALLBACK');
act(() => set(myAtom, 'VALUE'));
expect(get(myAtom)).toBe('VALUE');
act(() => set(myAtom, null));
expect(get(myAtom)).toBe(null);
act(() => set(myAtom, undefined));
expect(get(myAtom)).toBe(undefined);
act(() => set(myAtom, 'VALUE'));
expect(get(myAtom)).toBe('VALUE');
});
testRecoil('Effects', () => {
let inited = false;
const myFallbackAtom = atom({
key: 'atom with fallback effects init fallback',
default: 'FALLBACK',
});
const myAtom = atom<string>({
key: 'atom with fallback effects init',
default: myFallbackAtom,
effects: [
({setSelf}) => {
inited = true;
setSelf('INIT');
},
],
});
expect(get(myAtom)).toEqual('INIT');
expect(inited).toEqual(true);
const [ReadsWritesAtom, _, reset] = componentThatReadsAndWritesAtom(myAtom);
const c = renderElements(<ReadsWritesAtom />);
expect(c.textContent).toEqual('"INIT"');
act(reset);
expect(c.textContent).toEqual('"FALLBACK"');
});

View File

@@ -0,0 +1,106 @@
/**
* 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 {RecoilValue} from '../../core/Recoil_RecoilValue';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let getRecoilValueAsLoadable, store, constSelector;
const testRecoil = getRecoilTestFn(() => {
const {
makeStore,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
({
getRecoilValueAsLoadable,
} = require('../../core/Recoil_RecoilValueInterface'));
constSelector = require('../Recoil_constSelector');
store = makeStore();
});
function get<T>(recoilValue: RecoilValue<T>): T {
return getRecoilValueAsLoadable<T>(store, recoilValue).valueOrThrow();
}
testRecoil('constSelector - string', () => {
const mySelector = constSelector('HELLO');
expect(get(mySelector)).toEqual('HELLO');
expect(get(mySelector)).toBe('HELLO');
});
testRecoil('constSelector - number', () => {
const mySelector = constSelector(42);
expect(get(mySelector)).toEqual(42);
expect(get(mySelector)).toBe(42);
});
testRecoil('constSelector - null', () => {
const mySelector = constSelector(null);
expect(get(mySelector)).toEqual(null);
expect(get(mySelector)).toBe(null);
});
testRecoil('constSelector - boolean', () => {
const mySelector = constSelector(true);
expect(get(mySelector)).toEqual(true);
expect(get(mySelector)).toBe(true);
});
testRecoil('constSelector - array', () => {
const emptyArraySelector = constSelector(([]: Array<$FlowFixMe>));
expect(get(emptyArraySelector)).toEqual([]);
const numberArray = [1, 2, 3];
const numberArraySelector = constSelector(numberArray);
expect(get(numberArraySelector)).toEqual([1, 2, 3]);
expect(get(numberArraySelector)).toBe(numberArray);
});
testRecoil('constSelector - object', () => {
const emptyObjSelector = constSelector({});
expect(get(emptyObjSelector)).toEqual({});
const obj = {foo: 'bar'};
const objSelector = constSelector(obj);
expect(get(objSelector)).toEqual({foo: 'bar'});
expect(get(objSelector)).toBe(obj);
// Calling a second time with same object provides the same selector
const objSelector2 = constSelector(obj);
expect(objSelector2).toBe(objSelector);
expect(get(objSelector2)).toEqual({foo: 'bar'});
expect(get(objSelector2)).toBe(obj);
// Calling a third time with similar but different object provides
// a new selector for the new reference.
const newObj = {foo: 'bar'};
const objSelector3 = constSelector(newObj);
expect(get(objSelector3)).toEqual({foo: 'bar'});
expect(get(objSelector3)).toBe(newObj);
});
testRecoil('constSelector - function', () => {
const foo = () => 'FOO';
const bar = () => 'BAR';
const fooSelector = constSelector(foo);
const barSelector = constSelector(bar);
expect(get(fooSelector)()).toEqual('FOO');
expect(get(barSelector)()).toEqual('BAR');
expect(constSelector(foo)).toEqual(fooSelector);
});

View File

@@ -0,0 +1,48 @@
/**
* 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 {RecoilValueReadOnly} from 'Recoil_RecoilValue';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let store, getRecoilValueAsLoadable, errorSelector;
const testRecoil = getRecoilTestFn(() => {
const {
makeStore,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
({
getRecoilValueAsLoadable,
} = require('../../core/Recoil_RecoilValueInterface'));
errorSelector = require('../Recoil_errorSelector');
store = makeStore();
});
function getError(recoilValue: RecoilValueReadOnly<mixed>): Error {
const error = getRecoilValueAsLoadable(store, recoilValue).errorOrThrow();
if (!(error instanceof Error)) {
throw new Error('Expected error to be an instance of Error');
}
return error;
}
testRecoil('errorSelector - string', () => {
const mySelector = errorSelector<mixed>('My Error');
expect(getError(mySelector).message).toEqual(
expect.stringContaining('My Error'),
);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,358 @@
/**
* 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 {RecoilValue} from '../../core/Recoil_RecoilValue';
import type {RecoilState} from 'Recoil_RecoilValue';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let atom,
DefaultValue,
selectorFamily,
getRecoilValueAsLoadable,
setRecoilValue,
store,
myAtom;
const testRecoil = getRecoilTestFn(() => {
const {
makeStore,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
atom = require('../Recoil_atom');
({DefaultValue} = require('../../core/Recoil_Node'));
selectorFamily = require('../Recoil_selectorFamily');
({
getRecoilValueAsLoadable,
setRecoilValue,
} = require('../../core/Recoil_RecoilValueInterface'));
store = makeStore();
myAtom = atom({
key: 'atom',
default: 0,
});
});
function getValue<T>(recoilValue: RecoilValue<T>): T {
return getRecoilValueAsLoadable<T>(store, recoilValue).valueOrThrow();
}
function set(recoilValue: RecoilState<number>, value: number) {
setRecoilValue(store, recoilValue, value);
}
testRecoil('selectorFamily - number parameter', () => {
const mySelector = selectorFamily({
key: 'selectorFamily/number',
get:
(
// $FlowFixMe[missing-local-annot]
multiplier,
) =>
// $FlowFixMe[missing-local-annot]
({get}) =>
get(myAtom) * multiplier,
});
set(myAtom, 1);
expect(getValue(mySelector(10))).toBe(10);
expect(getValue(mySelector(100))).toBe(100);
set(myAtom, 2);
expect(getValue(mySelector(10))).toBe(20);
expect(getValue(mySelector(100))).toBe(200);
});
testRecoil('selectorFamily - array parameter', () => {
const mySelector = selectorFamily({
key: 'selectorFamily/array',
// $FlowFixMe[missing-local-annot]
get: numbers => () => numbers.reduce((x, y) => x + y, 0),
});
expect(getValue(mySelector([]))).toBe(0);
expect(getValue(mySelector([1, 2, 3]))).toBe(6);
expect(getValue(mySelector([0, 1, 1, 2, 3, 5]))).toBe(12);
});
testRecoil('selectorFamily - object parameter', () => {
const mySelector = selectorFamily({
key: 'selectorFamily/object',
get:
(
// $FlowFixMe[missing-local-annot]
{multiplier},
) =>
// $FlowFixMe[missing-local-annot]
({get}) =>
get(myAtom) * multiplier,
});
set(myAtom, 1);
expect(getValue(mySelector({multiplier: 10}))).toBe(10);
expect(getValue(mySelector({multiplier: 100}))).toBe(100);
set(myAtom, 2);
expect(getValue(mySelector({multiplier: 10}))).toBe(20);
expect(getValue(mySelector({multiplier: 100}))).toBe(200);
});
testRecoil('selectorFamily - date parameter', () => {
const mySelector = selectorFamily({
key: 'selectorFamily/date',
get:
(
// $FlowFixMe[missing-local-annot]
date,
) =>
// $FlowFixMe[missing-local-annot]
({get}) => {
const daysToAdd = get(myAtom);
const returnDate = new Date(date);
returnDate.setDate(returnDate.getDate() + daysToAdd);
return returnDate;
},
});
set(myAtom, 1);
expect(getValue(mySelector(new Date(2021, 2, 25))).getDate()).toBe(26);
set(myAtom, 2);
expect(getValue(mySelector(new Date(2021, 2, 25))).getDate()).toBe(27);
});
testRecoil('Works with supersets', () => {
const mySelector = selectorFamily({
key: 'selectorFamily/supersets',
get:
(
// $FlowFixMe[missing-local-annot]
{multiplier},
) =>
// $FlowFixMe[missing-local-annot]
({get}) =>
get(myAtom) * multiplier,
});
set(myAtom, 1);
expect(getValue(mySelector({multiplier: 10}))).toBe(10);
expect(getValue(mySelector({multiplier: 100}))).toBe(100);
expect(getValue(mySelector({multiplier: 100, extra: 'foo'}))).toBe(100);
});
testRecoil('selectorFamily - writable', () => {
const mySelector = selectorFamily({
key: 'selectorFamily/writable',
get:
(
// $FlowFixMe[missing-local-annot]
{multiplier},
) =>
// $FlowFixMe[missing-local-annot]
({get}) =>
get(myAtom) * multiplier,
set:
(
// $FlowFixMe[missing-local-annot]
{multiplier},
) =>
// $FlowFixMe[missing-local-annot]
({set}, num) =>
set(myAtom, num instanceof DefaultValue ? num : num / multiplier),
});
set(myAtom, 1);
expect(getValue(mySelector({multiplier: 10}))).toBe(10);
set(mySelector({multiplier: 10}), 20);
expect(getValue(myAtom)).toBe(2);
set(mySelector({multiplier: 10}), 30);
expect(getValue(myAtom)).toBe(3);
set(mySelector({multiplier: 100}), 400);
expect(getValue(myAtom)).toBe(4);
});
testRecoil('selectorFamily - value caching', () => {
let evals = 0;
const mySelector = selectorFamily({
key: 'selectorFamily/value caching',
get:
(
// $FlowFixMe[missing-local-annot]
{multiplier},
) =>
// $FlowFixMe[missing-local-annot]
({get}) => {
evals++;
return get(myAtom) * multiplier;
},
});
expect(evals).toBe(0);
set(myAtom, 1);
expect(getValue(mySelector({multiplier: 10}))).toBe(10);
expect(evals).toBe(1);
expect(getValue(mySelector({multiplier: 10}))).toBe(10);
expect(evals).toBe(1);
expect(getValue(mySelector({multiplier: 100}))).toBe(100);
expect(evals).toBe(2);
expect(getValue(mySelector({multiplier: 100}))).toBe(100);
expect(evals).toBe(2);
expect(getValue(mySelector({multiplier: 10}))).toBe(10);
expect(evals).toBe(2);
set(myAtom, 2);
expect(getValue(mySelector({multiplier: 10}))).toBe(20);
expect(evals).toBe(3);
expect(getValue(mySelector({multiplier: 10}))).toBe(20);
expect(evals).toBe(3);
expect(getValue(mySelector({multiplier: 100}))).toBe(200);
expect(evals).toBe(4);
expect(getValue(mySelector({multiplier: 100}))).toBe(200);
expect(evals).toBe(4);
});
testRecoil('selectorFamily - reference caching', () => {
let evals = 0;
const mySelector = selectorFamily({
key: 'selectorFamily/reference caching',
get:
(
// $FlowFixMe[missing-local-annot]
{multiplier},
) =>
// $FlowFixMe[missing-local-annot]
({get}) => {
evals++;
return get(myAtom) * multiplier;
},
cachePolicyForParams_UNSTABLE: {
equality: 'reference',
},
});
expect(evals).toBe(0);
set(myAtom, 1);
expect(getValue(mySelector({multiplier: 10}))).toBe(10);
expect(evals).toBe(1);
expect(getValue(mySelector({multiplier: 10}))).toBe(10);
expect(evals).toBe(2);
expect(getValue(mySelector({multiplier: 100}))).toBe(100);
expect(evals).toBe(3);
expect(getValue(mySelector({multiplier: 100}))).toBe(100);
expect(evals).toBe(4);
expect(getValue(mySelector({multiplier: 10}))).toBe(10);
expect(evals).toBe(5);
set(myAtom, 2);
expect(getValue(mySelector({multiplier: 10}))).toBe(20);
expect(evals).toBe(6);
expect(getValue(mySelector({multiplier: 10}))).toBe(20);
expect(evals).toBe(7);
expect(getValue(mySelector({multiplier: 100}))).toBe(200);
expect(evals).toBe(8);
expect(getValue(mySelector({multiplier: 100}))).toBe(200);
expect(evals).toBe(9);
const multiply10 = {multiplier: 10};
const multiply100 = {multiplier: 100};
set(myAtom, 1);
expect(getValue(mySelector(multiply10))).toBe(10);
expect(evals).toBe(10);
expect(getValue(mySelector(multiply10))).toBe(10);
expect(evals).toBe(10);
expect(getValue(mySelector(multiply100))).toBe(100);
expect(evals).toBe(11);
expect(getValue(mySelector(multiply100))).toBe(100);
expect(evals).toBe(11);
expect(getValue(mySelector(multiply10))).toBe(10);
expect(evals).toBe(11);
set(myAtom, 2);
expect(getValue(mySelector(multiply10))).toBe(20);
expect(evals).toBe(12);
expect(getValue(mySelector(multiply10))).toBe(20);
expect(evals).toBe(12);
expect(getValue(mySelector(multiply100))).toBe(200);
expect(evals).toBe(13);
expect(getValue(mySelector(multiply100))).toBe(200);
expect(evals).toBe(13);
});
// Parameterized selector results should be frozen unless
// dangerouslyAllowMutability is set
testRecoil('selectorFamily - mutability', () => {
const myImmutableSelector = selectorFamily({
key: 'selectorFamily/immutable',
get:
(
// $FlowFixMe[missing-local-annot]
{key},
) =>
// $FlowFixMe[missing-local-annot]
({get}) => ({[key]: get(myAtom)}),
});
set(myAtom, 42);
const immutableResult: {[string]: number, ...} = getValue(
myImmutableSelector({key: 'foo'}),
);
expect(immutableResult).toEqual({foo: 42});
expect(() => {
immutableResult.foo = 2600;
}).toThrow();
const myMutableSelector = selectorFamily({
key: 'selectorFamily/mutable',
get:
(
// $FlowFixMe[missing-local-annot]
{key},
) =>
// $FlowFixMe[missing-local-annot]
({get}) => ({[key]: get(myAtom)}),
dangerouslyAllowMutability: true,
});
set(myAtom, 42);
const mutableResult: {[string]: number, ...} = getValue(
myMutableSelector({key: 'foo'}),
);
expect(mutableResult).toEqual({foo: 42});
mutableResult.foo = 2600;
expect(mutableResult).toEqual({foo: 2600});
});
testRecoil('selectorFamily - evaluate to RecoilValue', () => {
const atomA = atom({key: 'selectorFamily/const atom A', default: 'A'});
const atomB = atom({key: 'selectorFamily/const atom B', default: 'B'});
const mySelector = selectorFamily<string, string>({
key: 'selectorFamily/',
get: param => () => param === 'a' ? atomA : atomB,
});
expect(getValue(mySelector('a'))).toEqual('A');
expect(getValue(mySelector('b'))).toEqual('B');
});
testRecoil('selectorFamily - invalid parameter error message', () => {
const mySelector = selectorFamily<_, {foo: () => void}>({
key: 'function in parameter',
get: () => () => {},
});
expect(() => getValue(mySelector({foo: () => {}}))).toThrow(
'function in parameter',
);
});

File diff suppressed because it is too large Load Diff