// ets_tracing: off /* eslint-disable prefer-const */ import * as A from "../Collections/Immutable/Array/index.js" import * as E from "../Either/index.js" import type { FiberID } from "../Fiber/id.js" import type { Trace } from "../Fiber/index.js" import { identity, pipe } from "../Function/index.js" import * as S from "../IO/index.js" import * as O from "../Option/index.js" import { Stack } from "../Stack/index.js" import type { Both, Cause, Then, Traced } from "./cause.js" import { combinePar, combineSeq, die, empty, fail, interrupt, traced } from "./cause.js" import { InterruptedException } from "./errors.js" export { combinePar, Cause, die, empty, fail, interrupt, combineSeq, traced, isEmpty } from "./cause.js" /** * Applicative's ap */ export function ap(fa: Cause): (fab: Cause<(a: A) => B>) => Cause { return chain((f) => pipe(fa, map(f))) } /** * Substitute the E in the cause */ export function as(e: E1) { return map(() => e) } /** * Builds a Cause depending on the result of another */ export function chain_(cause: Cause, f: (_: E) => Cause): Cause { return S.run(chainSafe_(cause, f)) } /** * Builds a Cause depending on the result of another */ export function chain(f: (_: E) => Cause) { return (cause: Cause): Cause => chain_(cause, f) } /** * Builds a Cause depending on the result of another */ export function chainSafe_( cause: Cause, f: (_: E) => Cause ): S.IO> { switch (cause._tag) { case "Empty": { return S.succeed(empty) } case "Fail": { return S.succeed(f(cause.value)) } case "Die": { return S.succeed(cause) } case "Interrupt": { return S.succeed(cause) } case "Then": { return S.zipWith_( S.suspend(() => chainSafe_(cause.left, f)), S.suspend(() => chainSafe_(cause.right, f)), (l, r) => combineSeq(l, r) ) } case "Both": { return S.zipWith_( S.suspend(() => chainSafe_(cause.left, f)), S.suspend(() => chainSafe_(cause.right, f)), (l, r) => combinePar(l, r) ) } case "Traced": { return S.map_(chainSafe_(cause.cause, f), (x) => traced(x, cause.trace)) } } } /** * Equivalent to chain((a) => Fail(f(a))) */ export function map_(cause: Cause, f: (e: E) => E1) { return chain_(cause, (e: E) => fail(f(e))) } /** * Equivalent to chain((a) => Fail(f(a))) */ export function map(f: (e: E) => E1) { return (cause: Cause) => map_(cause, f) } /** * Determines if this cause contains or is equal to the specified cause. */ export function contains(that: Cause) { return (cause: Cause) => S.run(containsSafe(that)(cause)) } /** * Determines if this cause contains or is equal to the specified cause. */ export function containsSafe(that: Cause) { return (cause: Cause) => S.gen(function* (_) { if (yield* _(cause.equalsSafe(that))) { return true } return yield* _( pipe( cause, reduceLeft(S.succeed(false))((_, c) => O.some(S.chain_(_, (b) => (b ? S.succeed(b) : c.equalsSafe(that)))) ) ) ) }) } /** * Extracts a list of non-recoverable errors from the `Cause`. */ export function defects(cause: Cause): readonly unknown[] { return pipe( cause, reduceLeft([])((a, c) => c._tag === "Die" ? O.some([...a, c.value]) : O.none ) ) } /** * Returns the `Error` associated with the first `Die` in this `Cause` if * one exists. */ export function dieOption(cause: Cause) { return pipe( cause, find((c) => (c._tag === "Die" ? O.some(c.value) : O.none)) ) } /** * Returns if a cause contains a defect */ export function died(cause: Cause) { return pipe( cause, dieOption, O.map(() => true), O.getOrElse(() => false) ) } /** * Returns the `E` associated with the first `Fail` in this `Cause` if one * exists. */ export function failureOption(cause: Cause) { return pipe( cause, find((c) => (c._tag === "Fail" ? O.some(c.value) : O.none)) ) } /** * Returns if the cause has a failure in it */ export function failed(cause: Cause) { return pipe( cause, failureOption, O.map(() => true), O.getOrElse(() => false) ) } /** * Retrieve the first checked error on the `Left` if available, * if there are no checked errors return the rest of the `Cause` * that is known to contain only `Die` or `Interrupt` causes. * */ export function failureOrCause(cause: Cause): E.Either> { return pipe( cause, failureOption, O.map(E.left), O.getOrElse(() => E.right(cause as Cause)) // no E inside this cause, can safely cast ) } /** * Produces a list of all recoverable errors `E` in the `Cause`. */ export function failures(cause: Cause) { return pipe( cause, reduceLeft([])((a, c) => c._tag === "Fail" ? O.some([...a, c.value]) : O.none ) ) } /** * Remove all `Die` causes that the specified partial function is defined at, * returning `Some` with the remaining causes or `None` if there are no * remaining causes. */ export function stripSomeDefects(f: (_: unknown) => O.Option) { return (cause: Cause): O.Option> => { return S.run(stripSomeDefectsSafe(cause, f)) } } /** * Remove all `Die` causes that the specified partial function is defined at, * returning `Some` with the remaining causes or `None` if there are no * remaining causes. */ export function stripSomeDefects_( cause: Cause, f: (_: unknown) => O.Option ): O.Option> { return S.run(stripSomeDefectsSafe(cause, f)) } /** * Filter out all `Die` causes according to the specified function, * returning `Some` with the remaining causes or `None` if there are no * remaining causes. */ export function stripSomeDefectsSafe( cause: Cause, f: (_: unknown) => O.Option ): S.IO>> { switch (cause._tag) { case "Empty": { return S.succeed(O.none) } case "Interrupt": { return S.succeed(O.some(cause)) } case "Fail": { return S.succeed(O.some(cause)) } case "Die": { return S.succeed(O.map_(f(cause.value), die)) } case "Both": { return S.zipWith_( S.suspend(() => stripSomeDefectsSafe(cause.left, f)), S.suspend(() => stripSomeDefectsSafe(cause.right, f)), (l, r) => { if (l._tag === "Some" && r._tag === "Some") { return O.some(combinePar(l.value, r.value)) } else if (l._tag === "Some") { return l } else if (r._tag === "Some") { return r } else { return O.none } } ) } case "Then": { return S.zipWith_( S.suspend(() => stripSomeDefectsSafe(cause.left, f)), S.suspend(() => stripSomeDefectsSafe(cause.right, f)), (l, r) => { if (l._tag === "Some" && r._tag === "Some") { return O.some(combineSeq(l.value, r.value)) } else if (l._tag === "Some") { return l } else if (r._tag === "Some") { return r } else { return O.none } } ) } case "Traced": { return S.suspend(() => stripSomeDefectsSafe(cause.cause, f)) } } } /** * Finds the first result matching f */ export function find( f: (cause: Cause) => O.Option ): (cause: Cause) => O.Option { return (cause) => S.run(findSafe(f)(cause)) } /** * Finds the first result matching f */ export function findSafe( f: (cause: Cause) => O.Option ): (cause: Cause) => S.IO> { return (cause) => { const apply = f(cause) if (apply._tag === "Some") { return S.succeed(apply) } switch (cause._tag) { case "Then": { return S.chain_( S.suspend(() => findSafe(f)(cause.left)), (isLeft) => { if (isLeft._tag === "Some") { return S.succeed(isLeft) } else { return findSafe(f)(cause.right) } } ) } case "Traced": { return S.suspend(() => findSafe(f)(cause.cause)) } case "Both": { return S.chain_( S.suspend(() => findSafe(f)(cause.left)), (isLeft) => { if (isLeft._tag === "Some") { return S.succeed(isLeft) } else { return findSafe(f)(cause.right) } } ) } default: { return S.succeed(apply) } } } } /** * Equivalent to chain(identity) */ export const flatten: (cause: Cause>) => Cause = chain(identity) /** * Folds over a cause */ export function fold( empty: () => Z, failCase: (_: E) => Z, dieCase: (_: unknown) => Z, interruptCase: (_: FiberID) => Z, thenCase: (_: Z, __: Z) => Z, bothCase: (_: Z, __: Z) => Z, tracedCase: (_: Z, __: Trace) => Z ) { return (cause: Cause): Z => S.run( foldSafe( empty, failCase, dieCase, interruptCase, thenCase, bothCase, tracedCase )(cause) ) } /** * Folds over a cause */ export function foldSafe( empty: () => Z, failCase: (_: E) => Z, dieCase: (_: unknown) => Z, interruptCase: (_: FiberID) => Z, thenCase: (_: Z, __: Z) => Z, bothCase: (_: Z, __: Z) => Z, tracedCase: (_: Z, __: Trace) => Z ) { return (cause: Cause): S.IO => { switch (cause._tag) { case "Empty": { return S.succeedWith(empty) } case "Fail": { return S.succeed(failCase(cause.value)) } case "Die": { return S.succeed(dieCase(cause.value)) } case "Interrupt": { return S.succeed(interruptCase(cause.fiberId)) } case "Traced": { return S.map_( S.suspend(() => foldSafe( empty, failCase, dieCase, interruptCase, thenCase, bothCase, tracedCase )(cause.cause) ), (x) => tracedCase(x, cause.trace) ) } case "Both": { return S.zipWith_( S.suspend(() => foldSafe( empty, failCase, dieCase, interruptCase, thenCase, bothCase, tracedCase )(cause.left) ), S.suspend(() => foldSafe( empty, failCase, dieCase, interruptCase, thenCase, bothCase, tracedCase )(cause.right) ), (l, r) => bothCase(l, r) ) } case "Then": { return S.zipWith_( S.suspend(() => foldSafe( empty, failCase, dieCase, interruptCase, thenCase, bothCase, tracedCase )(cause.left) ), S.suspend(() => foldSafe( empty, failCase, dieCase, interruptCase, thenCase, bothCase, tracedCase )(cause.right) ), (l, r) => thenCase(l, r) ) } } } } /** * Accumulates a state over a Cause */ export function reduceLeft(z: Z) { return (f: (z: Z, cause: Cause) => O.Option): ((cause: Cause) => Z) => { return (cause) => { let causes: Stack> | undefined = undefined let current: Cause | undefined = cause let acc = z while (current) { const x = f(acc, current) acc = x._tag === "Some" ? x.value : acc switch (current._tag) { case "Then": { causes = new Stack(current.right, causes) current = current.left break } case "Both": { causes = new Stack(current.right, causes) current = current.left break } case "Traced": { current = current.cause break } default: { current = undefined break } } if (!current && causes) { current = causes.value causes = causes.previous } } return acc } } } /** * Returns if the cause contains an interruption in it */ export function interrupted(cause: Cause) { return pipe( cause, interruptOption, O.map(() => true), O.getOrElse(() => false) ) } /** * Returns the `FiberID` associated with the first `Interrupt` in this `Cause` if one * exists. */ export function interruptOption(cause: Cause) { return pipe( cause, find((c) => (c._tag === "Interrupt" ? O.some(c.fiberId) : O.none)) ) } /** * Determines if the `Cause` contains only interruptions and not any `Die` or * `Fail` causes. */ export function interruptedOnly(cause: Cause) { return pipe( cause, find((c) => (c._tag === "Die" || c._tag === "Fail" ? O.some(false) : O.none)), O.getOrElse(() => true) ) } /** * Returns a set of interruptors, fibers that interrupted the fiber described * by this `Cause`. */ export function interruptors(cause: Cause): readonly FiberID[] { return Array.from( pipe( cause, reduceLeft>(new Set())((s, c) => c._tag === "Interrupt" ? O.some(s.add(c.fiberId)) : O.none ) ) ) } /** * Remove all `Fail` and `Interrupt` nodes from this `Cause`, * return only `Die` cause/finalizer defects. */ export function keepDefectsSafe(cause: Cause): S.IO>> { switch (cause._tag) { case "Empty": { return S.succeed(O.none) } case "Fail": { return S.succeed(O.none) } case "Interrupt": { return S.succeed(O.none) } case "Die": { return S.succeed(O.some(cause)) } case "Traced": { return S.map_( S.suspend(() => keepDefectsSafe(cause.cause)), (x) => O.map_(x, (_) => traced(_, cause.trace)) ) } case "Then": { return S.zipWith_( S.suspend(() => keepDefectsSafe(cause.left)), S.suspend(() => keepDefectsSafe(cause.right)), (l, r) => { if (l._tag === "Some" && r._tag === "Some") { return O.some(combineSeq(l.value, r.value)) } else if (l._tag === "Some") { return l } else if (r._tag === "Some") { return r } else { return O.none } } ) } case "Both": { return S.zipWith_( S.suspend(() => keepDefectsSafe(cause.left)), S.suspend(() => keepDefectsSafe(cause.right)), (l, r) => { if (l._tag === "Some" && r._tag === "Some") { return O.some(combinePar(l.value, r.value)) } else if (l._tag === "Some") { return l } else if (r._tag === "Some") { return r } else { return O.none } } ) } } } /** * Remove all `Fail` and `Interrupt` nodes from this `Cause`, * return only `Die` cause/finalizer defects. */ export function keepDefects(cause: Cause): O.Option> { return S.run(keepDefectsSafe(cause)) } /** * Converts the specified `Cause>` to an `Either, A>`. */ export function sequenceCauseEither( c: Cause> ): E.Either, A> { return S.run(sequenceCauseEitherSafe(c)) } /** * Converts the specified `Cause>` to an `Either, A>`. */ export function sequenceCauseEitherSafe( c: Cause> ): S.IO, A>> { switch (c._tag) { case "Empty": { return S.succeed(E.left(empty)) } case "Interrupt": { return S.succeed(E.left(c)) } case "Fail": { return S.succeed( c.value._tag === "Left" ? E.left(fail(c.value.left)) : E.right(c.value.right) ) } case "Traced": { return S.map_( S.suspend(() => sequenceCauseEitherSafe(c.cause)), (x) => E.mapLeft_(x, (_) => traced(_, c.trace)) ) } case "Die": { return S.succeed(E.left(c)) } case "Then": { return S.zipWith_( S.suspend(() => sequenceCauseEitherSafe(c.left)), S.suspend(() => sequenceCauseEitherSafe(c.right)), (l, r) => { if (l._tag === "Left") { if (r._tag === "Right") { return E.right(r.right) } else { return E.left(combineSeq(l.left, r.left)) } } else { return E.right(l.right) } } ) } case "Both": { return S.zipWith_( S.suspend(() => sequenceCauseEitherSafe(c.left)), S.suspend(() => sequenceCauseEitherSafe(c.right)), (l, r) => { if (l._tag === "Left") { if (r._tag === "Right") { return E.right(r.right) } else { return E.left(combinePar(l.left, r.left)) } } else { return E.right(l.right) } } ) } } } /** * Converts the specified `Cause>` to an `Option>` by * recursively stripping out any failures with the error `None`. */ export function sequenceCauseOptionSafe( c: Cause> ): S.IO>> { switch (c._tag) { case "Empty": { return S.succeed(O.some(empty)) } case "Interrupt": { return S.succeed(O.some(c)) } case "Traced": { return S.map_( S.suspend(() => sequenceCauseOptionSafe(c.cause)), (x) => O.map_(x, (_) => traced(_, c.trace)) ) } case "Fail": { return S.succeed(O.map_(c.value, fail)) } case "Die": { return S.succeed(O.some(c)) } case "Then": { return S.zipWith_( S.suspend(() => sequenceCauseOptionSafe(c.left)), S.suspend(() => sequenceCauseOptionSafe(c.right)), (l, r) => { if (l._tag === "Some" && r._tag === "Some") { return O.some(combineSeq(l.value, r.value)) } else if (l._tag === "Some") { return O.some(l.value) } else if (r._tag === "Some") { return O.some(r.value) } else { return O.none } } ) } case "Both": { return S.zipWith_( S.suspend(() => sequenceCauseOptionSafe(c.left)), S.suspend(() => sequenceCauseOptionSafe(c.right)), (l, r) => { if (l._tag === "Some" && r._tag === "Some") { return O.some(combinePar(l.value, r.value)) } else if (l._tag === "Some") { return O.some(l.value) } else if (r._tag === "Some") { return O.some(r.value) } else { return O.none } } ) } } } /** * Converts the specified `Cause>` to an `Option>` by * recursively stripping out any failures with the error `None`. */ export function sequenceCauseOption(c: Cause>): O.Option> { return S.run(sequenceCauseOptionSafe(c)) } /** * Squashes a `Cause` down to a single `Throwable`, chosen to be the * "most important" `Throwable`. */ export function squash(f: (e: E) => unknown) { return (cause: Cause): unknown => pipe( cause, failureOption, O.map(f), (o) => o._tag === "Some" ? o : interrupted(cause) ? O.some( new InterruptedException( "Interrupted by fibers: " + Array.from(interruptors(cause)) .map((_) => _.seqNumber.toString()) .map((_) => "#" + _) .join(", ") ) ) : O.none, (o) => (o._tag === "Some" ? o : A.head(defects(cause))), O.getOrElse(() => new InterruptedException()) ) } /** * Discards all typed failures kept on this `Cause`. */ export function stripFailures(cause: Cause): Cause { switch (cause._tag) { case "Empty": { return empty } case "Fail": { return empty } case "Interrupt": { return cause } case "Die": { return cause } default: { return S.run(stripFailuresSafe(cause)) } } } /** * Discards all typed failures kept on this `Cause`. */ export function stripFailuresSafe(cause: Cause): S.IO> { switch (cause._tag) { case "Empty": { return S.succeed(empty) } case "Fail": { return S.succeed(empty) } case "Interrupt": { return S.succeed(cause) } case "Die": { return S.succeed(cause) } case "Traced": { return S.map_( S.suspend(() => stripFailuresSafe(cause.cause)), (x) => traced(x, cause.trace) ) } case "Both": { return S.zipWith_( S.suspend(() => stripFailuresSafe(cause.left)), S.suspend(() => stripFailuresSafe(cause.right)), (l, r) => combinePar(l, r) ) } case "Then": { return S.zipWith_( S.suspend(() => stripFailuresSafe(cause.left)), S.suspend(() => stripFailuresSafe(cause.right)), (l, r) => combineSeq(l, r) ) } } } /** * Discards all typed failures kept on this `Cause`. */ export function stripInterrupts(cause: Cause): Cause { switch (cause._tag) { case "Empty": { return empty } case "Fail": { return cause } case "Interrupt": { return empty } case "Die": { return cause } default: { return S.run(stripInterruptsSafe(cause)) } } } /** * Discards all typed failures kept on this `Cause`. */ export function stripInterruptsSafe(cause: Cause): S.IO> { switch (cause._tag) { case "Empty": { return S.succeed(empty) } case "Fail": { return S.succeed(cause) } case "Interrupt": { return S.succeed(empty) } case "Die": { return S.succeed(cause) } case "Traced": { return S.map_( S.suspend(() => stripInterruptsSafe(cause.cause)), (x) => traced(x, cause.trace) ) } case "Both": { return S.zipWith_( S.suspend(() => stripInterruptsSafe(cause.left)), S.suspend(() => stripInterruptsSafe(cause.right)), (l, r) => combinePar(l, r) ) } case "Then": { return S.zipWith_( S.suspend(() => stripInterruptsSafe(cause.left)), S.suspend(() => stripInterruptsSafe(cause.right)), (l, r) => combineSeq(l, r) ) } } } /** * Returns a `Cause` that has been stripped of all tracing information. */ export function untraced(cause: Cause): Cause { switch (cause._tag) { case "Die": case "Empty": case "Fail": case "Interrupt": return cause default: return S.run(untracedSafe(cause)) } } /** * Returns a `Cause` that has been stripped of all tracing information. */ export function untracedSafe(cause: Cause): S.IO> { switch (cause._tag) { case "Traced": { return S.suspend(() => untracedSafe(cause.cause)) } case "Both": { return S.zipWith_( S.suspend(() => untracedSafe(cause.left)), S.suspend(() => untracedSafe(cause.right)), (l, r) => combinePar(l, r) ) } case "Then": { return S.zipWith_( S.suspend(() => untracedSafe(cause.left)), S.suspend(() => untracedSafe(cause.right)), (l, r) => combineSeq(l, r) ) } default: { return S.succeed(cause) } } } const FCOStackFrameDoneTypeId = Symbol() class FCOStackFrameDone { readonly _typeId: typeof FCOStackFrameDoneTypeId = FCOStackFrameDoneTypeId } const FCOStackFrameTracedTypeId = Symbol() class FCOStackFrameTraced { readonly _typeId: typeof FCOStackFrameTracedTypeId = FCOStackFrameTracedTypeId constructor(readonly cause: Traced>) {} } const FCOStackFrameThenLeftTypeId = Symbol() class FCOStackFrameThenLeft { readonly _typeId: typeof FCOStackFrameThenLeftTypeId = FCOStackFrameThenLeftTypeId constructor(readonly cause: Then>) {} } const FCOStackFrameThenRightTypeId = Symbol() class FCOStackFrameThenRight { readonly _typeId: typeof FCOStackFrameThenRightTypeId = FCOStackFrameThenRightTypeId constructor( readonly cause: Then>, readonly leftResult: O.Option> ) {} } const FCOStackFrameBothLeftTypeId = Symbol() class FCOStackFrameBothLeft { readonly _typeId: typeof FCOStackFrameBothLeftTypeId = FCOStackFrameBothLeftTypeId constructor(readonly cause: Both>) {} } const FCOStackFrameBothRightTypeId = Symbol() class FCOStackFrameBothRight { readonly _typeId: typeof FCOStackFrameBothRightTypeId = FCOStackFrameBothRightTypeId constructor( readonly cause: Both>, readonly leftResult: O.Option> ) {} } type FCOStackFrame = | FCOStackFrameDone | FCOStackFrameTraced | FCOStackFrameThenLeft | FCOStackFrameThenRight | FCOStackFrameBothLeft | FCOStackFrameBothRight /** * Converts the specified `Cause>` to an `Either, A>` by * recursively stripping out any failures with the error `None`. */ export function flipCauseOption(c: Cause>): O.Option> { let stack: Stack> = new Stack(new FCOStackFrameDone()) let result: O.Option> | undefined recursion: while (stack) { // eslint-disable-next-line no-constant-condition pushing: while (true) { switch (c._tag) { case "Empty": result = O.some(empty) break pushing case "Traced": stack = new Stack(new FCOStackFrameTraced(c), stack) c = c.cause continue pushing case "Interrupt": result = O.some(interrupt(c.fiberId)) break pushing case "Die": result = O.some(c) break pushing case "Fail": result = O.fold_( c.value, () => O.none, (r) => O.some(fail(r)) ) break pushing case "Then": stack = new Stack(new FCOStackFrameThenLeft(c), stack) c = c.left continue pushing case "Both": stack = new Stack(new FCOStackFrameBothLeft(c), stack) c = c.left continue pushing } } // eslint-disable-next-line no-constant-condition popping: while (true) { const top = stack.value stack = stack.previous! switch (top._typeId) { case FCOStackFrameDoneTypeId: return result case FCOStackFrameTracedTypeId: result = O.map_(result, (_) => traced(_, top.cause.trace)) continue popping case FCOStackFrameThenLeftTypeId: c = top.cause.right stack = new Stack(new FCOStackFrameThenRight(top.cause, result), stack) continue recursion case FCOStackFrameThenRightTypeId: { const l = top.leftResult if (O.isSome(l) && O.isSome(result)) { result = O.some(combineSeq(l.value, result.value)) } if (O.isNone(l) && O.isSome(result)) { result = O.some(result.value) } if (O.isSome(l) && O.isNone(result)) { result = O.some(l.value) } result = O.none continue popping } case FCOStackFrameBothLeftTypeId: c = top.cause.right stack = new Stack(new FCOStackFrameBothRight(top.cause, result), stack) continue recursion case FCOStackFrameBothRightTypeId: { const l = top.leftResult if (O.isSome(l) && O.isSome(result)) { result = O.some(combinePar(l.value, result.value)) } if (O.isNone(l) && O.isSome(result)) { result = O.some(result.value) } if (O.isSome(l) && O.isNone(result)) { result = O.some(l.value) } result = O.none continue popping } } } } throw new Error("Bug") }