UNPKG

redux-pixies

Version:

The magical asynchronous Redux library

174 lines (155 loc) 4.21 kB
// @flow import type { Condition, PixieInput, PixieInstance, TamePixie, TamePixieInput, UpdateFunction, WildPixie } from '../types.js' function makePixieShutdownError() { const e = new Error('Pixie has been destroyed') e.name = 'PixieShutdownError' return e } export function isPixieShutdownError(e: any) { return e instanceof Error && e.name === 'PixieShutdownError' } /** * If a wild pixie returns a bare function, turn that into a proper object. */ function fixInstance<P>( instance: PixieInstance<P> | UpdateFunction<P> ): PixieInstance<P> { if (typeof instance === 'function') { return { update: instance, destroy() {} } } return instance } /** * Catches synchronous errors and sends them through `onError`, * terminating the inner pixie in response. Also prevents `update` * from running in parallel if if returns a promise. */ export function babysitPixie<P>(wildPixie: WildPixie<P>): TamePixie<P> { function outPixie(input: TamePixieInput) { let instance: PixieInstance<P> | void let propsCache: P let propsDirty: boolean = true let updating: boolean = false let destroyed: boolean = false let nextPromise: Promise<P> | void let rejector: ((e: any) => void) | void let resolver: ((props: P) => void) | void function destroy() { if (instance) { try { if (rejector) { const copy = rejector nextPromise = undefined rejector = undefined resolver = undefined copy(makePixieShutdownError()) } const copy = instance instance = undefined copy.destroy() } catch (e) { onError(e) } destroyed = true } } // Ignore any callbacks once `destroy` has completed: function onError(e: Error) { if (!destroyed) input.onError(e) destroy() } function onOutput(data: any) { if (!destroyed) input.onOutput(data) } function onUpdateDone() { updating = false tryUpdate() } function tryUpdate() { // eslint-disable-next-line no-unmodified-loop-condition while (instance && propsDirty && !updating) { propsDirty = false updating = true try { const thenable = instance.update(propsCache) if (thenable && typeof thenable.then === 'function') { thenable.then(onUpdateDone, onError) } else { updating = false } } catch (e) { onError(e) } } } function getNextPromise(): Promise<P> { if (!nextPromise) { nextPromise = new Promise((resolve, reject) => { resolver = resolve rejector = reject }) } return nextPromise } const childInput: PixieInput<P> = { onError, onOutput, get props() { return propsCache }, nextProps: getNextPromise, waitFor<R>(condition: Condition<P, R>): Promise<R> { return new Promise((resolve, reject) => { function checkProps() { try { const result = condition(propsCache) if (result != null) resolve(result) else getNextPromise().then(checkProps, reject) } catch (e) { reject(e) } } return checkProps() }) } } try { instance = fixInstance(wildPixie(childInput)) } catch (e) { onError(e) } return { update(props: P) { propsCache = props propsDirty = true // Update the `nextProps` promise right away: if (resolver) { const copy = resolver nextPromise = undefined rejector = undefined resolver = undefined copy(props) } tryUpdate() }, destroy } } outPixie.tame = true outPixie.defaultOutput = wildPixie.defaultOutput return outPixie } /** * Accepts a hand-written reducer, and hardens it with error checking. */ export function tamePixie<P>(pixie: WildPixie<P>): TamePixie<P> { return pixie.tame ? (pixie: any) : babysitPixie(pixie) }