packages/recoil/recoil_values/Recoil_WaitFor.js (244 lines of code) (raw):
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails oncall+recoil
* @flow strict-local
* @format
*/
'use strict';
import type {Loadable} from '../adt/Recoil_Loadable';
import type {
RecoilValue,
RecoilValueReadOnly,
} from '../core/Recoil_RecoilValue';
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, deps) {
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) {
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>},
results,
) {
return Array.isArray(dependencies)
? results
: // Object.getOwnPropertyNames() has consistent key ordering with ES6
Object.getOwnPropertyNames(dependencies).reduce(
(out, key, idx) => ({...out, [key]: results[idx]}),
{},
);
}
function wrapLoadables(
dependencies:
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>
| {+[string]: RecoilValueReadOnly<mixed>},
results,
exceptions,
) {
const output = exceptions.map((exception, idx) =>
exception == null
? loadableWithValue(results[idx])
: isPromise(exception)
? loadableWithPromise(exception)
: loadableWithError(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>, ...}>,
// $FlowFixMe[incompatible-type-arg]
// $FlowFixMe[incompatible-type] added when improving typing for this parameters
> = 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, ...}>,
// $FlowFixMe[incompatible-type] added when improving typing for this parameters
> = 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, ...}>,
// $FlowFixMe[incompatible-type] added when improving typing for this parameters
> = 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, ...}>,
// $FlowFixMe[incompatible-type] added when improving typing for this parameters
> = 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>,
// $FlowFixMe[incompatible-type] added when improving typing for this parameters
) => 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,
};