| 1 | /** |
| 2 | * Models one low-level pull step for stream-like consumers. |
| 3 | * |
| 4 | * A `Pull<A, E, Done, R>` is an `Effect` that can produce one `A`, fail with an |
| 5 | * ordinary error `E`, or signal end-of-input with `Cause.Done<Done>`. The |
| 6 | * separate done signal lets low-level consumers distinguish normal completion |
| 7 | * from failure. This module includes type extractors and helpers for detecting, |
| 8 | * filtering, catching, converting, and matching done signals separately from |
| 9 | * ordinary failures. |
| 10 | * |
| 11 | * @since 4.0.0 |
| 12 | */ |
| 13 | import * as Cause from "./Cause.ts" |
| 14 | import type { Effect } from "./Effect.ts" |
| 15 | import * as Exit from "./Exit.ts" |
| 16 | import * as Filter from "./Filter.ts" |
| 17 | import { dual } from "./Function.ts" |
| 18 | import * as internalEffect from "./internal/effect.ts" |
| 19 | import * as Result from "./Result.ts" |
| 20 | |
| 21 | /** |
| 22 | * An effectful pull step that either produces a value, fails with `E`, or |
| 23 | * signals completion with `Cause.Done<Done>`. |
| 24 | * |
| 25 | * **When to use** |
| 26 | * |
| 27 | * Use to model one low-level pull step when a consumer repeatedly evaluates an |
| 28 | * effect that may emit a value, fail normally, or signal normal completion |
| 29 | * through `Cause.Done`. |
| 30 | * |
| 31 | * **Details** |
| 32 | * |
| 33 | * `Pull` represents completion in the error channel so low-level stream |
| 34 | * consumers can distinguish ordinary failures from end-of-input and carry a |
| 35 | * leftover value when needed. |
| 36 | * |
| 37 | * @category models |
| 38 | * @since 4.0.0 |
| 39 | */ |
| 40 | export interface Pull<out A, out E = never, out Done = void, out R = never> |
| 41 | extends Effect<A, E | Cause.Done<Done>, R> |
| 42 | {} |
| 43 | |
| 44 | /** |
| 45 | * Extracts the success type from a Pull type. |
| 46 | * |
| 47 | * **When to use** |
| 48 | * |
| 49 | * Use to derive the value produced by an existing `Pull` when declaring |
| 50 | * reusable type aliases, low-level stream helpers, or function signatures. |
| 51 | * |
| 52 | * @see {@link Error} for extracting the ordinary failure type |
| 53 | * @see {@link Leftover} for extracting the completion leftover type |
| 54 | * @see {@link Services} for extracting the required services type instead |
| 55 | * |
| 56 | * @category utility types |
| 57 | * @since 4.0.0 |
| 58 | */ |
| 59 | export type Success<P> = P extends Effect<infer _A, infer _E, infer _R> ? _A : never |
| 60 | |
| 61 | /** |
| 62 | * Extracts the error type from a Pull type, excluding Done errors. |
| 63 | * |
| 64 | * **When to use** |
| 65 | * |
| 66 | * Use to derive only the ordinary failure type from a `Pull` when declaring |
| 67 | * wrappers or APIs that handle completion separately. |
| 68 | * |
| 69 | * @see {@link Success} for extracting the pulled value type instead |
| 70 | * @see {@link Leftover} for extracting the completion leftover type |
| 71 | * @see {@link Services} for extracting the required services type instead |
| 72 | * @see {@link ExcludeDone} for excluding `Cause.Done` from an error union |
| 73 | * |
| 74 | * @category utility types |
| 75 | * @since 4.0.0 |
| 76 | */ |
| 77 | export type Error<P> = P extends Effect<infer _A, infer _E, infer _R> ? _E extends Cause.Done<infer _L> ? never : _E |
| 78 | : never |
| 79 | |
| 80 | /** |
| 81 | * Extracts the leftover type from a Pull type. |
| 82 | * |
| 83 | * **When to use** |
| 84 | * |
| 85 | * Use to derive the completion leftover type from an existing `Pull` when |
| 86 | * declaring reusable type aliases or helper signatures that preserve a pull's |
| 87 | * done value. |
| 88 | * |
| 89 | * @see {@link Success} for extracting the pulled value type instead |
| 90 | * @see {@link Error} for extracting the ordinary failure type, excluding `Cause.Done` |
| 91 | * @see {@link Services} for extracting the required services type instead |
| 92 | * |
| 93 | * @category utility types |
| 94 | * @since 4.0.0 |
| 95 | */ |
| 96 | export type Leftover<P> = P extends Effect<infer _A, infer _E, infer _R> ? _E extends Cause.Done<infer _L> ? _L : never |
| 97 | : never |
| 98 | |
| 99 | /** |
| 100 | * Extracts the service requirements (context) type from a Pull type. |
| 101 | * |
| 102 | * **When to use** |
| 103 | * |
| 104 | * Use to derive the context requirements of a generic or inferred `Pull` |
| 105 | * without restating its `R` type parameter. |
| 106 | * |
| 107 | * @see {@link Success} for extracting the pulled value type instead |
| 108 | * @see {@link Error} for extracting the ordinary failure type |
| 109 | * @see {@link Leftover} for extracting the completion leftover type |
| 110 | * |
| 111 | * @category utility types |
| 112 | * @since 4.0.0 |
| 113 | */ |
| 114 | export type Services<P> = P extends Effect<infer _A, infer _E, infer _R> ? _R : never |
| 115 | |
| 116 | /** |
| 117 | * Excludes `Cause.Done` completion signals from an error type union. |
| 118 | * |
| 119 | * **When to use** |
| 120 | * |
| 121 | * Use to describe the ordinary error type that remains after `Cause.Done` |
| 122 | * completion signals have been handled or filtered out of an error union. |
| 123 | * |
| 124 | * @see {@link Error} for extracting ordinary failures from a `Pull` |
| 125 | * @see {@link Leftover} for extracting the completion leftover type |
| 126 | * |
| 127 | * @category utility types |
| 128 | * @since 4.0.0 |
| 129 | */ |
| 130 | export type ExcludeDone<E> = Exclude<E, Cause.Done<any>> |
| 131 | |
| 132 | // ----------------------------------------------------------------------------- |
| 133 | // Done |
| 134 | // ----------------------------------------------------------------------------- |
| 135 | |
| 136 | /** |
| 137 | * Handles `Cause.Done` failures in an effect while leaving ordinary failures |
| 138 | * in the error channel. |
| 139 | * |
| 140 | * **When to use** |
| 141 | * |
| 142 | * Use to recover from a `Cause.Done` completion signal in an effect, such as |
| 143 | * turning a pull leftover value into a successful recovery effect while |
| 144 | * preserving ordinary failures. |
| 145 | * |
| 146 | * **Details** |
| 147 | * |
| 148 | * The handler receives the done leftover value and may recover with a new |
| 149 | * effect. Non-done errors are preserved. |
| 150 | * |
| 151 | * @see {@link matchEffect} for handling success, ordinary failure, and done outcomes explicitly |
| 152 | * @see {@link filterDoneLeftover} for extracting a done leftover from an existing `Cause` |
| 153 | * |
| 154 | * @category error handling |
| 155 | * @since 4.0.0 |
| 156 | */ |
| 157 | export const catchDone: { |
| 158 | <E, A2, E2, R2>(f: (leftover: Cause.Done.Extract<E>) => Effect<A2, E2, R2>): <A, R>( |
| 159 | self: Effect<A, E, R> |
| 160 | ) => Effect<A | A2, ExcludeDone<E> | E2, R | R2> |
| 161 | <A, R, E, A2, E2, R2>( |
| 162 | self: Effect<A, E, R>, |
| 163 | f: (leftover: Cause.Done.Extract<E>) => Effect<A2, E2, R2> |
| 164 | ): Effect<A | A2, ExcludeDone<E> | E2, R | R2> |
| 165 | } = dual(2, <A, R, E, A2, E2, R2>( |
| 166 | effect: Effect<A, E, R>, |
| 167 | f: (leftover: Cause.Done.Extract<E>) => Effect<A2, E2, R2> |
| 168 | ): Effect<A | A2, ExcludeDone<E> | E2, R | R2> => |
| 169 | internalEffect.catchCauseFilter(effect, filterDoneLeftover as any, (l: any) => f(l)) as any) |
| 170 | |
| 171 | /** |
| 172 | * Checks whether a Cause contains any done errors. |
| 173 | * |
| 174 | * **When to use** |
| 175 | * |
| 176 | * Use when you need to test whether a pull failure cause represents normal |
| 177 | * completion and only need a boolean result. |
| 178 | * |
| 179 | * @see {@link isDoneFailure} for checking a single `Cause.Reason` |
| 180 | * @see {@link filterDone} for extracting the `Cause.Done` value from a `Cause` |
| 181 | * @see {@link filterNoDone} for selecting causes with no done failures |
| 182 | * |
| 183 | * @category predicates |
| 184 | * @since 4.0.0 |
| 185 | */ |
| 186 | export const isDoneCause = <E>(cause: Cause.Cause<E>): boolean => cause.reasons.some(isDoneFailure) |
| 187 | |
| 188 | /** |
| 189 | * Checks whether a `Cause.Reason` is a `Fail` reason whose error is a |
| 190 | * `Cause.Done` signal. |
| 191 | * |
| 192 | * **When to use** |
| 193 | * |
| 194 | * Use when you need to identify done completion reasons while traversing |
| 195 | * `cause.reasons`, before handling ordinary failures. |
| 196 | * |
| 197 | * @see {@link isDoneCause} for checking an entire `Cause` for any done reason |
| 198 | * @see {@link filterDone} for extracting the `Cause.Done` value from a `Cause` |
| 199 | * |
| 200 | * @category guards |
| 201 | * @since 4.0.0 |
| 202 | */ |
| 203 | export const isDoneFailure = <E>( |
| 204 | failure: Cause.Reason<E> |
| 205 | ): failure is Cause.Fail<E & Cause.Done<any>> => failure._tag === "Fail" && Cause.isDone(failure.error) |
| 206 | |
| 207 | /** |
| 208 | * Finds a `Cause.Done` failure in a `Cause`. |
| 209 | * |
| 210 | * **When to use** |
| 211 | * |
| 212 | * Use to separate `Cause.Done` completion from ordinary causes while preserving |
| 213 | * the typed done value. |
| 214 | * |
| 215 | * **Details** |
| 216 | * |
| 217 | * Returns a successful `Result` with the `Cause.Done` value when the cause |
| 218 | * contains a done signal and no other failures besides interruptions. When the |
| 219 | * done signal was merged with a real failure (for example a failing |
| 220 | * finalizer), the `Result` fails with the remaining cause, stripped of the |
| 221 | * done signal. Without a done signal the `Result` fails with the original |
| 222 | * cause. |
| 223 | * |
| 224 | * @category filtering |
| 225 | * @since 4.0.0 |
| 226 | */ |
| 227 | export const filterDone: <E>( |
| 228 | input: Cause.Cause<E> |
| 229 | ) => Result.Result<Cause.Done.Only<E>, Cause.Cause<ExcludeDone<E>>> = <E>( |
| 230 | cause: Cause.Cause<E> |
| 231 | ): Result.Result<any, any> => { |
| 232 | let done: Cause.Done<any> | undefined |
| 233 | let hasFailure = false |
| 234 | for (const reason of cause.reasons) { |
| 235 | if (isDoneFailure(reason)) { |
| 236 | done ??= reason.error |
| 237 | } else if (reason._tag !== "Interrupt") { |
| 238 | hasFailure = true |
| 239 | } |
| 240 | } |
| 241 | if (done === undefined) return Result.fail(cause) |
| 242 | return hasFailure |
| 243 | ? Result.fail(Cause.fromReasons(cause.reasons.filter((reason) => !isDoneFailure(reason)))) |
| 244 | : Result.succeed(done) |
| 245 | } |
| 246 | |
| 247 | /** |
| 248 | * Finds a `Cause.Done` failure in a cause whose done value is not used. |
| 249 | * |
| 250 | * **When to use** |
| 251 | * |
| 252 | * Use to detect `Cause.Done` completion in a `Cause` when the completion value |
| 253 | * is not part of the downstream logic. |
| 254 | * |
| 255 | * **Details** |
| 256 | * |
| 257 | * Returns a successful `Result` with the done marker when it is the only |
| 258 | * failure, otherwise returns a failed `Result` with the non-done cause. |
| 259 | * |
| 260 | * @see {@link filterDone} for preserving the typed `Cause.Done` value when the done payload matters |
| 261 | * @see {@link filterDoneLeftover} for extracting only the done leftover value |
| 262 | * @see {@link filterNoDone} for the inverse filter that succeeds only when no done failure is present |
| 263 | * |
| 264 | * @category filtering |
| 265 | * @since 4.0.0 |
| 266 | */ |
| 267 | export const filterDoneVoid: <E extends Cause.Done>( |
| 268 | input: Cause.Cause<E> |
| 269 | ) => Result.Result<Cause.Done, Cause.Cause<Exclude<E, Cause.Done>>> = filterDone as any |
| 270 | |
| 271 | /** |
| 272 | * Keeps a `Cause` only when it contains no `Cause.Done` failures. |
| 273 | * |
| 274 | * **When to use** |
| 275 | * |
| 276 | * Use to select ordinary failure causes for handling while leaving `Cause.Done` |
| 277 | * completion causes outside that handler. |
| 278 | * |
| 279 | * **Details** |
| 280 | * |
| 281 | * Returns a successful `Result` with the cause when every failure is non-done; |
| 282 | * otherwise returns a failed `Result` with the original cause. |
| 283 | * |
| 284 | * @see {@link filterDone} for the inverse typed done filter |
| 285 | * @see {@link filterDoneVoid} for done detection when the payload is not needed |
| 286 | * |
| 287 | * @category filtering |
| 288 | * @since 4.0.0 |
| 289 | */ |
| 290 | export const filterNoDone: <E>( |
| 291 | input: Cause.Cause<E> |
| 292 | ) => Result.Result< |
| 293 | Cause.Cause<ExcludeDone<E>>, |
| 294 | Cause.Cause<E> |
| 295 | > = Filter.fromPredicate((cause: Cause.Cause<unknown>) => |
| 296 | cause.reasons.every((failure) => !isDoneFailure(failure)) |
| 297 | ) as any |
| 298 | |
| 299 | /** |
| 300 | * Filters a Cause to extract the leftover value from done errors. |
| 301 | * |
| 302 | * **When to use** |
| 303 | * |
| 304 | * Use to extract only the leftover value carried by a `Cause.Done` completion |
| 305 | * signal. |
| 306 | * |
| 307 | * @category filtering |
| 308 | * @since 4.0.0 |
| 309 | */ |
| 310 | export const filterDoneLeftover: <E>( |
| 311 | cause: Cause.Cause<E> |
| 312 | ) => Result.Result<Cause.Done.Extract<E>, Cause.Cause<ExcludeDone<E>>> = ((cause: Cause.Cause<any>) => { |
| 313 | const done = filterDone(cause) |
| 314 | return Result.isFailure(done) ? done : Result.succeed(done.success.value) |
| 315 | }) as any |
| 316 | |
| 317 | /** |
| 318 | * Converts a `Cause` into an `Exit`, treating `Cause.Done` as successful |
| 319 | * completion. |
| 320 | * |
| 321 | * **When to use** |
| 322 | * |
| 323 | * Use to produce an `Exit` for finalizing a low-level pull workflow when a |
| 324 | * `Cause.Done` signal should be treated as success and any remaining cause |
| 325 | * should fail. |
| 326 | * |
| 327 | * **Details** |
| 328 | * |
| 329 | * If the done signal is the only failure in the cause, its leftover becomes |
| 330 | * the successful value. Otherwise the non-done cause becomes the failure |
| 331 | * cause. |
| 332 | * |
| 333 | * @see {@link filterDone} for extracting the done signal without converting the cause to an `Exit` |
| 334 | * @see {@link matchEffect} for handling `Pull` success, failure, and done outcomes directly |
| 335 | * |
| 336 | * @category converting |
| 337 | * @since 4.0.0 |
| 338 | */ |
| 339 | export const doneExitFromCause = <E>(cause: Cause.Cause<E>): Exit.Exit<Cause.Done.Extract<E>, ExcludeDone<E>> => { |
| 340 | const halt = filterDone(cause) |
| 341 | return !Result.isFailure(halt) ? Exit.succeed(halt.success.value as any) : Exit.failCause(halt.failure) |
| 342 | } |
| 343 | |
| 344 | /** |
| 345 | * Pattern matches on a Pull, handling success, failure, and done cases. |
| 346 | * |
| 347 | * **When to use** |
| 348 | * |
| 349 | * Use to handle all three `Pull` outcomes with effectful handlers. |
| 350 | * |
| 351 | * **Example** (Matching Pull outcomes) |
| 352 | * |
| 353 | * ```ts import.meta.vitest |
| 354 | * import { Cause, Effect, Pull } from "effect" |
| 355 | * |
| 356 | * const pull = Cause.done("stream ended") |
| 357 | * |
| 358 | * const result = Pull.matchEffect(pull, { |
| 359 | * onSuccess: (value) => Effect.succeed(`Got value: ${value}`), |
| 360 | * onFailure: (cause) => Effect.succeed(`Got error: ${cause}`), |
| 361 | * onDone: (leftover) => Effect.succeed(`Stream halted with: ${leftover}`) |
| 362 | * }) |
| 363 | * |
| 364 | * await Effect.runPromise(result) // => "Stream halted with: stream ended" |
| 365 | * ``` |
| 366 | * |
| 367 | * @category pattern matching |
| 368 | * @since 4.0.0 |
| 369 | */ |
| 370 | export const matchEffect: { |
| 371 | <A, E, L, AS, ES, RS, AF, EF, RF, AH, EH, RH>(options: { |
| 372 | readonly onSuccess: (value: A) => Effect<AS, ES, RS> |
| 373 | readonly onFailure: (failure: Cause.Cause<E>) => Effect<AF, EF, RF> |
| 374 | readonly onDone: (leftover: L) => Effect<AH, EH, RH> |
| 375 | }): <R>(self: Pull<A, E, L, R>) => Effect<AS | AF | AH, ES | EF | EH, R | RS | RF | RH> |
| 376 | <A, E, L, R, AS, ES, RS, AF, EF, RF, AH, EH, RH>(self: Pull<A, E, L, R>, options: { |
| 377 | readonly onSuccess: (value: A) => Effect<AS, ES, RS> |
| 378 | readonly onFailure: (failure: Cause.Cause<E>) => Effect<AF, EF, RF> |
| 379 | readonly onDone: (leftover: L) => Effect<AH, EH, RH> |
| 380 | }): Effect<AS | AF | AH, ES | EF | EH, R | RS | RF | RH> |
| 381 | } = dual(2, <A, E, L, R, AS, ES, RS, AF, EF, RF, AH, EH, RH>(self: Pull<A, E, L, R>, options: { |
| 382 | readonly onSuccess: (value: A) => Effect<AS, ES, RS> |
| 383 | readonly onFailure: (failure: Cause.Cause<E>) => Effect<AF, EF, RF> |
| 384 | readonly onDone: (leftover: L) => Effect<AH, EH, RH> |
| 385 | }): Effect<AS | AF | AH, ES | EF | EH, R | RS | RF | RH> => |
| 386 | internalEffect.matchCauseEffect(self, { |
| 387 | onSuccess: options.onSuccess, |
| 388 | onFailure: (cause): Effect<AS | AF | AH, ES | EF | EH, RS | RF | RH> => { |
| 389 | const halt = filterDone(cause) |
| 390 | return !Result.isFailure(halt) ? options.onDone(halt.success.value as L) : options.onFailure(halt.failure) |
| 391 | } |
| 392 | })) |