import { array } from 'fp-ts/lib/Array' import * as E from 'fp-ts/lib/Either' import { unsafeCoerce } from 'fp-ts/lib/function' import { pipe } from 'fp-ts/lib/pipeable' import * as RTE from 'fp-ts/lib/ReaderTaskEither' import * as TE from 'fp-ts/lib/TaskEither' import { EmptyEnv, Subtract, } from '@monorail/sharedHelpers/fp-ts-ext/effectsUtils' import { noOpIO } from '@monorail/sharedHelpers/fp-ts-ext/IO' import { unsafeCoerceToArray } from '@monorail/sharedHelpers/fp-ts-ext/ReadonlyArray' export * from 'fp-ts/lib/ReaderTaskEither' /** * Pipeable port of rte.orElse, which widens types * @param f */ export const orElseW = ( f: (e: E) => RTE.ReaderTaskEither, ) => ( rte: RTE.ReaderTaskEither, ): RTE.ReaderTaskEither => pipe(rte, RTE.orElse(unsafeCoerce(f))) export const chainWFirst = ( f: (a: A) => RTE.ReaderTaskEither, ) => ( ma: RTE.ReaderTaskEither, ): RTE.ReaderTaskEither => env => () => RTE.run(ma, env).then(ea => pipe( ea, E.fold | Promise>>( err => E.left(err), a => RTE.run(f(a), env).then(() => ea), ), ), ) /** * Widens Dependency type to the manually supplied type parameter, * * i.e. * @example * ```ts * const rte = withEnv<{ num: number }>(RTE.of("foo")) * rte.run({ num: 6 }) // requires a `{ num: number }` * ``` */ export const withEnv = () => ( r: RTE.ReaderTaskEither, ): RTE.ReaderTaskEither => pipe( RTE.ask(), RTE.chainW(() => r), ) /** * Takes a successful Right RTE, and turns it into a failing 'Left' RTE * @param rtea */ export const toLeft = ( rtea: RTE.ReaderTaskEither, ): RTE.ReaderTaskEither => env => () => { return RTE.run(rtea, env).then( E.fold( e => E.left(e), a => E.left(a), ), ) } /** * Executes an RTE if a left is encountered, enabling access to the original error. * The value returnded from `f` will be discarded, and the resulting RTE will contain * the original value. * * @example * ```ts * pipe( * RTE.left2v('foo'), * onLeft(e => log(`Encountered error: ${e}`)) * ) * ``` */ export const onLeft = ( f: (e: E) => RTE.ReaderTaskEither, ) => (rte: RTE.ReaderTaskEither): RTE.ReaderTaskEither => pipe( rte, orElseW(e => { return pipe( toLeft(f(e)), RTE.mapLeft(() => e), ) }), ) /** * Runs a ReaderTaskEither with the specified parameters * * @example * ```ts * pipe( * RTE.ask(), * run('supplied parameter') * ) * ``` * * Note: if the inferred type of R is a subtype of the R in your RTE, * You'll need to explicitly type R to the supertype which matches the RTE * * This is because in fp-ts v1, the `R` type parameter in RTE is invariant * in fp-ts v2, it is contravariant, so this inference issue will go away. * * i.e.: * ```ts * declare const foo: 'foo' * pipe(RTE.ask(), run(foo)) // fails * pipe(RTE.ask(), run(foo)) // succeeds * ``` * * * Some more information about this error: * ```ts * * declare const fooRTE: RTE.ReaderTaskEither<'foo', unknown, unknown> * declare const stringRTE: RTE.ReaderTaskEither * * const a: RTE.ReaderTaskEither = fooRTE * const b: RTE.ReaderTaskEither<'foo', unknown, unknown> = fooRTE * const c: RTE.ReaderTaskEither<'foo', unknown, unknown> = stringRTE * const d: RTE.ReaderTaskEither = stringRTE * ``` * Here, the assignment to `a` and `b` fails, but in fp-ts v2, only the assignment to `a` fails * * Using the suffix `P` for `pipeable` to disambiguate the name from the base `run` * * NOTE: The above examples show primitive types being used in the RTE environment. However, * all utility functions in this file require the environment to be expressed as an object type * due to the reliance on using intersection types to aggregate dependencies. */ export const runP = (r: R) => ( rte: RTE.ReaderTaskEither, ): Promise> => RTE.run(rte, r) /** * A ReaderTaskEither that performs a noOp computation * * It uses an empty environment and cannot fail. */ export const noOpRTE = RTE.fromIO(noOpIO) /** * Provides the required environment to a ReaderTaskEither, * converting it into a TaskEither. * * Similar to `runP` but delays execution. */ export const provide = (r: R) => ( rte: RTE.ReaderTaskEither, ): TE.TaskEither => () => RTE.run(rte, r) /** * Provides a subset of a ReaderTaskEither's required environment, * returning a new ReaderTaskEither with a narrowed environment requirement. * * Similar to `provide` but does not completely fulfill the RTE's requirements. * Think of this as partial application for RTE dependencies. */ export const providePartial = (q: Q) => ( rte: RTE.ReaderTaskEither, ): RTE.ReaderTaskEither, E, A> => (r: Subtract) => () => RTE.run(rte, { ...q, ...r } as Q & R) /** * Given a tuple/list of RTEs, it will aggregate their combined environments into an intersection, * their combined errors into a union, and map the values into a corresponding tuple/list. * * TLDR; Promise.all for ReaderTaskEither */ export function sequenceW( rtes: Rtes, ): CombinedRtes { return (array.sequence(RTE.readerTaskEither)( unsafeCoerceToArray(rtes), ) as unknown) as CombinedRtes } /** * Performs the type-level computation that combineRTE uses */ export type CombinedRtes = RTE.ReaderTaskEither< CombinedRteEnv, CombinedRteErr, CombinedRteOutput > /** * Aggregate many RTE environments into an intersection. */ export type CombinedRteEnv< RTES extends DEFAULT_RTES > = unknown extends ToRteConsList ? ExtractRteEnv : UnNest, unknown>> // RTES = [RTE, RTE, RTE] // ToRteConsList = [A, [D, [F]]] // Flatten = [[[A & D & F]]] // UnNest = A & D & F export type CombinedRteErr = { [K in keyof RTES]: ExtractRteError }[number] export type CombinedRteOutput = { [K in keyof RTES]: ExtractRteOutput } // eslint-disable-next-line @typescript-eslint/no-explicit-any type SafeAny = any // Retrieve the environment from an RTE export type ExtractRteEnv = A extends RTE.ReaderTaskEither< infer R, SafeAny, SafeAny > ? R : never // Retrieve the error type from an RTE export type ExtractRteError = A extends RTE.ReaderTaskEither< SafeAny, infer R, SafeAny > ? R : never // Retrieve the output value from an RTE export type ExtractRteOutput = A extends RTE.ReaderTaskEither< SafeAny, SafeAny, infer R > ? R : never // Internal type DEFAULT_RTE = RTE.ReaderTaskEither type DEFAULT_RTES = ReadonlyArray // Creates a ConsList of RTE environment types type ToRteConsList = [] extends A ? unknown : ((...a: A) => SafeAny) extends (t: infer T, ...ts: infer TS) => SafeAny ? TS extends DEFAULT_RTES ? [ExtractRteEnv, ToRteConsList] : never : never // Recursively traverses a ConsList, creating an intersection as it does. type Flatten = A extends [infer H] ? S & H : A extends [infer I, infer T] ? [Flatten] // This tuple nesting is required to ensure that the type is lazily defined. : S // Since Flatten produces an n-depth Tuple containing the union we want, we need to traverse // that n-depth tuple and try to pull out our intersection type UnNest = T extends ReadonlyArray ? { [K in keyof T]: T[K] extends [infer TT] ? TT extends ReadonlyArray ? UnNest : TT : T[K] }[number] : Fallback