UNPKG

recoil

Version:

Recoil - A state management library for React

174 lines (149 loc) 5.6 kB
/** * 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. * * A type that represents a value that may or may not be loaded. It differs from * LoadObject in that the `loading` state has a Promise that is meant to resolve * when the value is available (but as with LoadObject, an individual Loadable * is a value type and is not mutated when the status of a request changes). * * @emails oncall+recoil * @flow strict * @format */ 'use strict'; import type { NodeKey } from '../core/Recoil_Keys'; const gkx = require('../util/Recoil_gkx'); const isPromise = require('../util/Recoil_isPromise'); const nullthrows = require('../util/Recoil_nullthrows'); // TODO Convert Loadable to a Class to allow for runtime type detection. // Containing static factories of withValue(), withError(), withPromise(), and all() export type ResolvedLoadablePromiseInfo<+T> = $ReadOnly<{ __value: T, __key?: NodeKey, }>; export type LoadablePromise<+T> = Promise<ResolvedLoadablePromiseInfo<T>>; type Accessors<T> = $ReadOnly<{ // Attempt to get the value. // If there's an error, throw an error. If it's still loading, throw a Promise // This is useful for composing with React Suspense or in a Recoil Selector. getValue: () => T, toPromise: () => Promise<T>, // Convenience accessors valueMaybe: () => T | void, valueOrThrow: () => T, errorMaybe: () => Error | void, errorOrThrow: () => Error, promiseMaybe: () => Promise<T> | void, promiseOrThrow: () => Promise<T>, map: <T, S>(map: (T) => Promise<S> | S) => Loadable<S>, }>; export type Loadable<+T> = $ReadOnly<{ state: 'hasValue', contents: T, ...Accessors<T>, }> | $ReadOnly<{ state: 'hasError', contents: Error, ...Accessors<T>, }> | $ReadOnly<{ state: 'loading', contents: LoadablePromise<T>, ...Accessors<T>, }>; type UnwrapLoadables<Loadables> = $TupleMap<Loadables, <T>(Loadable<T>) => T>; const loadableAccessors = { /** * if loadable has a value (state === 'hasValue'), return that value. * Otherwise, throw the (unwrapped) promise or the error. */ getValue() { if (this.state === 'loading' && gkx('recoil_async_selector_refactor')) { throw (this.contents: Promise<$FlowFixMe>).then(({ __value }) => __value); } if (this.state !== 'hasValue') { throw this.contents; } return this.contents; }, toPromise(): Promise<$FlowFixMe> { return this.state === 'hasValue' ? Promise.resolve(this.contents) : this.state === 'hasError' ? Promise.reject(this.contents) : gkx('recoil_async_selector_refactor') ? (this.contents: Promise<$FlowFixMe>).then(({ __value }) => __value) : this.contents; }, valueMaybe() { return this.state === 'hasValue' ? this.contents : undefined; }, valueOrThrow() { if (this.state !== 'hasValue') { throw new Error(`Loadable expected value, but in "${this.state}" state`); } return this.contents; }, errorMaybe() { return this.state === 'hasError' ? this.contents : undefined; }, errorOrThrow() { if (this.state !== 'hasError') { throw new Error(`Loadable expected error, but in "${this.state}" state`); } return this.contents; }, promiseMaybe(): void | Promise<$FlowFixMe> { return this.state === 'loading' ? gkx('recoil_async_selector_refactor') ? (this.contents: Promise<$FlowFixMe>).then(({ __value }) => __value) : this.contents : undefined; }, promiseOrThrow(): Promise<$FlowFixMe> { if (this.state !== 'loading') { throw new Error(`Loadable expected promise, but in "${this.state}" state`); } return gkx('recoil_async_selector_refactor') ? (this.contents: Promise<$FlowFixMe>).then(({ __value }) => __value) : (this.contents: Promise<$FlowFixMe>); }, // TODO Unit tests // TODO Convert Loadable to a Class to better support chaining // by returning a Loadable from a map function map<T, S>(map: (T) => LoadablePromise<S> | S): Loadable<S> { if (this.state === 'hasError') { return this; } if (this.state === 'hasValue') { try { const next = map(this.contents); // TODO if next instanceof Loadable, then return next return isPromise(next) ? loadableWithPromise(next) : loadableWithValue(next); } catch (e) { return isPromise(e) ? // If we "suspended", then try again. // errors and subsequent retries will be handled in 'loading' case loadableWithPromise(e.next(() => map(this.contents))) : loadableWithError(e); } } if (this.state === 'loading') { return loadableWithPromise(this.contents // TODO if map returns a loadable, then return the value or promise or throw the error .then(map).catch(e => { if (isPromise(e)) { // we were "suspended," try again return e.then(() => map(this.contents)); } throw e; })); } throw new Error('Invalid Loadable state'); } }; declare function loadableWithValue<T>(value: T): Loadable<T>; declare function loadableWithError<T>(error: Error): Loadable<T>; declare function loadableWithPromise<T>(promise: LoadablePromise<T>): Loadable<T>; declare function loadableLoading<T>(): Loadable<T>; declare function loadableAll<Inputs: $ReadOnlyArray<Loadable<mixed>>>(inputs: Inputs): Loadable<UnwrapLoadables<Inputs>>; module.exports = { loadableWithValue, loadableWithError, loadableWithPromise, loadableLoading, loadableAll };