// @flow // A task that either returns, or errors export opaque type Task< +Arg, +Inbound, +InboundError = mixed, +Result = Inbound, +Error = InboundError >: $ReadOnly<{ // This declaration is the public API type: string, payload: Arg, map( successTransform: (Result) => ResultPrime ): Task, bimap( successTransform: (Result) => ResultPrime, errorTransform: (Error) => ErrorPrime ): Task, chain( chainTransform: (Result) => Task<*, *, *, ResultPrime, ErrorPrime> ): Task }> = $ReadOnly<{ // This declaration is the private API. kind: 'regular', type: string, payload: Arg, /* * This is a little tricky. This `run` takes a lambda and calls either * the success or error handlers based on the result. We need this so * we can substitute applying effectful functions for mocking results * in test. */ run( BiApplicative, (Result) => mixed, (Error) => mixed, context: mixed ): mixed, map( successTransform: (Result) => ResultPrime ): Task, bimap( successTransform: (Result) => ResultPrime, errorTransform: (Error) => ErrorPrime ): Task, chain( chainTransform: (Result) => Task<*, *, *, ResultPrime, ErrorPrime> ): Task }>; // A function that does some side-effect when run. export type Effector = ( (Inbound) => mixed, (InboundError) => mixed, context: mixed ) => mixed; // A function that runs an effector for some environment. // In test, we provide one that doesn't call the effectful // function, instead providing a mock response. export type BiApplicative = ( Effector, (S) => mixed, (E) => mixed, mixed ) => mixed; // Private API for running a task. Do not use this directly. // We need this because Task is an opaque type, and we // hide `.run` outside this file. export function _run( task: Task<*, Inbound, InboundError, Result, ErrorT>, fnApplication: BiApplicative, success: Result => mixed, error: ErrorT => mixed, context?: mixed ): mixed { if (typeof task.run !== 'function') { throw new Error('Attempted to run something that is not a task.'); } return task.run(fnApplication, success, error, context); } /* * A function that takes some Arg and returns a new task. */ export type TaskCreator< Arg, +Inbound, +InboundError = mixed, -Result = Inbound, -Error = InboundError > = (Arg => Task) & $ReadOnly<{|type: string|}>; /** * A group of tasks, all of different types */ export type AnyTask = Task; export type AnyTasks = $ReadOnlyArray; /** * Tasks whose type must be disambiguated from their use * (because they were dynamically hoisted using `withTask`, for instance). */ export type MixedTask = Task; export type MixedTasks = $ReadOnlyArray; type Callback = ((err: Error) => mixed) & ((err: void, result: Result) => mixed); /** * ## `Task.fromCallback` * Returns a task-creator from a function that returns a promise. * * `arg => Promise` -> `arg => Task`. * * Uses the second arg as a label for debugging. */ export function fromPromise( fn: Arg => Promise, label: string ): TaskCreator { const creator = outbound => taskCreator_( (success, error) => fn(outbound).then(success, error), outbound, label ); creator.type = label; return (creator: any); } const noop = () => {}; /** * ## `Task.fromCallbackWithProgress` * Returns a task-creator from a function that returns a promise. * * `({arg, onProgress}) => Promise` -> `({arg, onProgress}) => Task`. * * Uses the second arg as a label for debugging. */ export function fromPromiseWithProgress( fn: ({arg: Arg, onProgress: any => void}) => Promise, label: string ): TaskCreator { const creator = ({arg, onProgress}) => { const task = taskCreator_( (success, error, context) => fn({ arg, onProgress: (context ? v => (context: any).onProgress(onProgress(v)) : noop) || noop }).then(success, error), {arg, onProgress}, label ); return task; }; creator.type = label; return (creator: any); } /** * `Task.fromCallback` * * Turn a node-style callback function: * `(arg, cb: (err, res) => void) => void`) * into a task creator of the same type. * * Uses the second arg as a label for debugging. */ export function fromCallback( fn: (Arg, Callback) => mixed, label: string ): TaskCreator { const creator = (outbound: Arg) => taskCreator_( (success, error) => fn(outbound, (err, result) => (err ? error(err) : success(result))), outbound, label ); creator.type = label; return (creator: any); } export type EffectReport = 'start' | 'success' | 'error'; /* * This is the private constructor for creating a Task object. End users * probably want to use `Task.fromCallback` or `task.fromPromise`. * It adds instrumentation to the effector, and also attaches some info * useful for making assertions in test. */ export function taskCreator_( effector: ( (Inbound) => mixed, (InboundError) => mixed, context?: mixed ) => mixed, payload: Arg, label: string ): Task { // Instrument the task with reporting const effectorPrime = (success, error, context) => { reportEffects('start', newTask, payload); return effector( result => { reportEffects('success', newTask, result); return success(result); }, reason => { reportEffects('error', newTask, reason); return error(reason); }, context ); }; effectorPrime.payload = payload; effectorPrime.type = label; const newTask = _task( payload, (runEffect, success, error, context) => runEffect(effectorPrime, success, error, context), label ); return newTask; } // Internal task constructor. // Note that payload is only kept around for testing/debugging purposes // It should not be introspected outside of test function _task( payload: Arg, next: ( runEffect: BiApplicative, (Result) => mixed, (Error) => mixed, context: mixed ) => mixed, label: string ): Task { return { label, type: label, payload, /* * Given the effector (or a mock), kicks off the task. * You (the end user) probably don't need to call this * directly. The middleware should handle it. */ run: next, /* * Public Task Methods */ chain, map, bimap }; function map( successTransform: Result => ResultPrime ): Task { return _task( payload, (runEffect, success, error, context) => next( runEffect, (result: Result) => success(successTransform(result)), error, context ), label ); } function bimap( successTransform: Result => ResultPrime, errorTransform: Error => ErrorPrime ): Task { return _task( payload, (runEffect, success, error, context) => next( runEffect, (result: Result) => success(successTransform(result)), (reason: Error) => error(errorTransform(reason)), context ), label ); } function chain( chainTransform: Result => Task<*, *, *, ResultPrime, ErrorPrime> ): Task { return _task( payload, (runEffect, success, error, context) => next( runEffect, (result: Result) => { const chainTask = chainTransform(result); return chainTask.run(runEffect, success, error, context); }, error, context ), `Chain(${label})` ); } } /* * Record the inputs/outputs of all tasks, for debugging or inspecting. * This feature should not be used to implement runtime behavior. */ let reportEffects: ( event: EffectReport, task: AnyTask, payload: mixed ) => void = (event: EffectReport, task: AnyTask, payload: mixed) => {}; /** * ## `reportTasksForTesting` * * Takes a function that is called whenever a task is dispatched, * returns, or errors. * * Note that only one function can be registered with this hook. * The last provided function is the one that takes effect. */ export function reportTasksForTesting( fn: (event: EffectReport, task: AnyTask, payload: mixed) => void ): void { reportEffects = fn; } // type level utils functions needed for Task.all type ExtractArg = (Task) => O; type ExtractResult = (Task<*, *, *, R>) => R; type ExtractError = (Task<*, *, *, *, E>) => E; /* * ## `Task.all` * * Given an array of Tasks, returns a new task that runs all the effects * of the original in parallel, with an array result where each element * corresponds to a task. * * Acts like `Promise.all`. */ export function all>>( tasks: AllTasks ): Task< $TupleMap, *, *, $TupleMap, mixed > { return _task( tasks.map(task => task.payload), ( runEffect, success: ($TupleMap) => mixed, error, context ) => { if (tasks.length === 0) { return success([]); } const accumulated = Array(tasks.length); let complete = 0; let errorValue = null; function allSuccess(index) { return value => { if (errorValue) { return; } accumulated[index] = value; complete += 1; if (complete === tasks.length) { return success(accumulated); } }; } function anyError(err) { if (!err) { return; } errorValue = err; return error(errorValue); } return Promise.all( tasks.map((task, index) => task.run(runEffect, allSuccess(index), anyError, context) ) ); }, 'Task.all(' + tasks.map(({type}) => type).join(', ') + ')' ); } type ExtractSettled = ( Task<*, *, *, R, E> ) => {|status: 'fulfilled', value: R|} | {|status: 'rejected', value: E|}; /* * ## `Task.allSettled` * * Given an array of Tasks, returns a new task that runs all the effects * of the original in parallel, with an array result where each element * corresponds to a task. * * Acts like `Promise.allSettled`. */ export function allSettled>>( tasks: AllTasks ): Task< $TupleMap, *, *, $TupleMap, mixed > { return _task( tasks.map(task => task.payload), ( runEffect, success: ($TupleMap) => mixed, error, context ) => { if (tasks.length === 0) { return success([]); } const accumulated = Array(tasks.length); let complete = 0; function onOneTaskFinish(index, status) { return value => { accumulated[index] = {status, value}; complete += 1; if (complete === tasks.length) { return success(accumulated); } }; } return (Promise: any).allSettled( tasks.map((task, index) => task.run( runEffect, onOneTaskFinish(index, 'fulfilled'), onOneTaskFinish(index, 'rejected'), context ) ) ); }, 'Task.allSettled(' + tasks.map(({type}) => type).join(', ') + ')' ); }