/** * 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> | {+[string]: RecoilValueReadOnly}, ): $ReadOnlyArray> { return Array.isArray(dependencies) ? dependencies : Object.getOwnPropertyNames(dependencies).map(key => dependencies[key]); } function wrapResults( dependencies: | $ReadOnlyArray> | {+[string]: RecoilValueReadOnly}, 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> | {+[string]: RecoilValueReadOnly}, 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( syncResults: Array, asyncResults: Array, ): Array { 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> | $ReadOnly<{[string]: RecoilValueReadOnly, ...}>, >( RecoilValues, ) => RecoilValueReadOnly< $ReadOnlyArray> | $ReadOnly<{[string]: Loadable, ...}>, // $FlowFixMe[incompatible-type-arg] // $FlowFixMe[incompatible-type] added when improving typing for this parameters > = selectorFamily({ key: '__waitForNone', get: ( dependencies: | $ReadOnly<{[string]: RecoilValueReadOnly}> | $ReadOnlyArray>, ) => ({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> | $ReadOnly<{[string]: RecoilValueReadOnly, ...}>, >( RecoilValues, ) => RecoilValueReadOnly< $ReadOnlyArray | $ReadOnly<{[string]: mixed, ...}>, // $FlowFixMe[incompatible-type] added when improving typing for this parameters > = selectorFamily({ key: '__waitForAny', get: ( dependencies: | $ReadOnly<{[string]: RecoilValueReadOnly}> | $ReadOnlyArray>, ) => ({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> | $ReadOnly<{[string]: RecoilValueReadOnly, ...}>, >( RecoilValues, ) => RecoilValueReadOnly< $ReadOnlyArray | $ReadOnly<{[string]: mixed, ...}>, // $FlowFixMe[incompatible-type] added when improving typing for this parameters > = selectorFamily({ key: '__waitForAll', get: ( dependencies: | $ReadOnly<{[string]: RecoilValueReadOnly}> | $ReadOnlyArray>, ) => ({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> | $ReadOnly<{[string]: RecoilValueReadOnly, ...}>, >( RecoilValues, ) => RecoilValueReadOnly< $ReadOnlyArray | $ReadOnly<{[string]: mixed, ...}>, // $FlowFixMe[incompatible-type] added when improving typing for this parameters > = selectorFamily({ key: '__waitForAllSettled', get: ( dependencies: | $ReadOnly<{[string]: RecoilValueReadOnly}> | $ReadOnlyArray>, ) => ({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, // $FlowFixMe[incompatible-type] added when improving typing for this parameters ) => RecoilValueReadOnly> = 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, };