UNPKG

recoil

Version:

Recoil - A state management library for React

168 lines (146 loc) 5.38 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 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, }>; declare class Canceled {} const CANCELED: Canceled = new Canceled(); export type LoadablePromise<+T> = Promise<ResolvedLoadablePromiseInfo<T> | Canceled>; 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 valueOrThrow: () => T, errorOrThrow: () => mixed, promiseOrThrow: () => Promise<T>, is: (Loadable<mixed>) => boolean, map: <T, S>(map: (T) => Promise<S> | S) => Loadable<S>, }>; type ValueAccessors<T> = $ReadOnly<{ ...Accessors<T>, valueMaybe: () => T, errorMaybe: () => void, promiseMaybe: () => void, }>; type ErrorAccessors<T> = $ReadOnly<{ ...Accessors<T>, valueMaybe: () => void, errorMaybe: () => mixed, promiseMaybe: () => void, }>; type LoadingAccessors<T> = $ReadOnly<{ ...Accessors<T>, valueMaybe: () => void, errorMaybe: () => void, promiseMaybe: () => Promise<T>, }>; type ValueLoadable<+T> = $ReadOnly<{ state: 'hasValue', contents: T, ...ValueAccessors<T>, }>; type ErrorLoadable<+T> = $ReadOnly<{ state: 'hasError', contents: mixed, ...ErrorAccessors<T>, }>; type LoadingLoadable<+T> = $ReadOnly<{ state: 'loading', contents: LoadablePromise<T>, ...LoadingAccessors<T>, }>; export type Loadable<+T> = ValueLoadable<T> | ErrorLoadable<T> | LoadingLoadable<T>; const loadableAccessors = { valueMaybe() { return undefined; }, valueOrThrow() { const error = new Error(`Loadable expected value, but in "${this.state}" state`); // V8 keeps closures alive until stack is accessed, this prevents a memory leak error.stack; throw error; }, errorMaybe() { return undefined; }, errorOrThrow() { const error = new Error(`Loadable expected error, but in "${this.state}" state`); // V8 keeps closures alive until stack is accessed, this prevents a memory leak error.stack; throw error; }, promiseMaybe() { return undefined; }, promiseOrThrow() { const error = new Error(`Loadable expected promise, but in "${this.state}" state`); // V8 keeps closures alive until stack is accessed, this prevents a memory leak error.stack; throw error; }, is(other: Loadable<mixed>): boolean { return other.state === this.state && other.contents === this.contents; }, // 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; })); } const error = new Error('Invalid Loadable state'); // V8 keeps closures alive until stack is accessed, this prevents a memory leak error.stack; throw error; } }; declare function loadableWithValue<T>(value: T): ValueLoadable<T>; declare function loadableWithError<T>(error: mixed): ErrorLoadable<T>; declare function loadableWithPromise<T>(promise: LoadablePromise<T>): LoadingLoadable<T>; declare function loadableLoading<T>(): Loadable<T>; type UnwrapLoadables<Loadables> = $TupleMap<Loadables, <T>(Loadable<T>) => T>; declare function loadableAll<Inputs: $ReadOnlyArray<Loadable<mixed>>>(inputs: Inputs): Loadable<UnwrapLoadables<Inputs>>; module.exports = { loadableWithValue, loadableWithError, loadableWithPromise, loadableLoading, loadableAll, Canceled, CANCELED };