1/**
2 * Represents observable state for asynchronous values.
3 *
4 * `AsyncResult<A, E>` records whether asynchronous work has no value yet,
5 * succeeded with an `A`, or failed with an `E`. Every state also carries a
6 * `waiting` flag, so callers can keep showing the current value while newer
7 * work is loading, refreshing, retrying, or recovering. This module includes
8 * constructors, checks, accessors, mapping and matching helpers, ways to combine
9 * several results, and schemas for encoding or decoding results.
10 *
11 * @stability unstable
12 * @since 4.0.0
13 */
14import * as Cause from "../Cause.ts"
15import * as Effect from "../Effect.ts"
16import * as Equal from "../Equal.ts"
17import * as Exit from "../Exit.ts"
18import type { LazyArg } from "../Function.ts"
19import { constTrue, dual, identity } from "../Function.ts"
20import * as Hash from "../Hash.ts"
21import * as InternalRecord from "../internal/record.ts"
22import * as Option from "../Option.ts"
23import { type Pipeable, pipeArguments } from "../Pipeable.ts"
24import type { Predicate, Refinement } from "../Predicate.ts"
25import { hasProperty, isIterable } from "../Predicate.ts"
26import * as Result from "../Result.ts"
27import * as Schema_ from "../Schema.ts"
28import * as SchemaIssue from "../SchemaIssue.ts"
29import * as SchemaParser from "../SchemaParser.ts"
30import * as SchemaTransformation from "../SchemaTransformation.ts"
31import type * as Types from "../Types.ts"
32
33/**
34 * Type-level identifier used to recognize `AsyncResult` values.
35 *
36 * @stability unstable
37 * @category type IDs
38 * @since 4.0.0
39 */
40export type TypeId = "~effect/reactivity/AsyncResult"
41
42/**
43 * Runtime identifier attached to `AsyncResult` values and used by `isAsyncResult`.
44 *
45 * @stability unstable
46 * @category type IDs
47 * @since 4.0.0
48 */
49export const TypeId: TypeId = "~effect/reactivity/AsyncResult"
50
51/**
52 * Represents the state of an asynchronous value as `Initial`, `Success`, or `Failure`, with a `waiting` flag for in-flight refreshes.
53 *
54 * @stability unstable
55 * @category models
56 * @since 4.0.0
57 */
58export type AsyncResult<A, E = never> = Initial<A, E> | Success<A, E> | Failure<A, E>
59
60/**
61 * Returns `true` when a value is an `AsyncResult`.
62 *
63 * @stability unstable
64 * @category guards
65 * @since 4.0.0
66 */
67export const isAsyncResult = (u: unknown): u is AsyncResult<unknown, unknown> => hasProperty(u, TypeId)
68
69/**
70 * Namespace containing type-level helpers and the shared prototype shape for `AsyncResult` values.
71 *
72 * @stability unstable
73 * @since 4.0.0
74 */
75export declare namespace AsyncResult {
76 /**
77 * Common prototype fields implemented by every `AsyncResult` variant, including pipeability, the type marker, phantom type members, and the `waiting` flag.
78 *
79 * @stability unstable
80 * @category models
81 * @since 4.0.0
82 */
83 export interface Proto<A, E> extends Pipeable {
84 readonly [TypeId]: {
85 readonly E: (_: never) => E
86 readonly A: (_: never) => A
87 }
88 readonly waiting: boolean
89 }
90
91 /**
92 * Extracts the success value type from an `AsyncResult`.
93 *
94 * @stability unstable
95 * @category utility types
96 * @since 4.0.0
97 */
98 export type Success<R> = R extends AsyncResult<infer A, infer _> ? A : never
99
100 /**
101 * Extracts the failure error type from an `AsyncResult`.
102 *
103 * @stability unstable
104 * @category utility types
105 * @since 4.0.0
106 */
107 export type Failure<R> = R extends AsyncResult<infer _, infer E> ? E : never
108}
109
110/**
111 * Rebuilds an `AsyncResult` with new success and failure types while preserving the variant of another result.
112 *
113 * @stability unstable
114 * @category utility types
115 * @since 4.0.0
116 */
117export type With<R extends AsyncResult<any, any>, A, E> = R extends Initial<infer _A, infer _E> ? Initial<A, E>
118 : R extends Success<infer _A, infer _E> ? Success<A, E>
119 : R extends Failure<infer _A, infer _E> ? Failure<A, E>
120 : never
121
122const ResultProto = {
123 [TypeId]: {
124 E: identity,
125 A: identity
126 },
127 pipe() {
128 return pipeArguments(this, arguments)
129 },
130 [Equal.symbol](this: AsyncResult<any, any>, that: AsyncResult<any, any>): boolean {
131 if (this._tag !== that._tag || this.waiting !== that.waiting) {
132 return false
133 }
134 switch (this._tag) {
135 case "Initial":
136 return true
137 case "Success":
138 return Equal.equals(this.value, (that as Success<any, any>).value)
139 case "Failure":
140 return Equal.equals(this.cause, (that as Failure<any, any>).cause)
141 }
142 },
143 [Hash.symbol](this: AsyncResult<any, any>): number {
144 const tagHash = Hash.string(`${this._tag}:${this.waiting}`)
145 if (this._tag === "Initial") {
146 return tagHash
147 }
148 return Hash.combine(tagHash)(this._tag === "Success" ? Hash.hash(this.value) : Hash.hash(this.cause))
149 }
150}
151
152/**
153 * Returns whether an `AsyncResult` is currently waiting for an asynchronous computation or refresh to finish.
154 *
155 * @stability unstable
156 * @category predicates
157 * @since 4.0.0
158 */
159export const isWaiting = <A, E>(result: AsyncResult<A, E>): boolean => result.waiting
160
161/**
162 * Initial `AsyncResult` state before a success value or failure cause is available.
163 *
164 * @stability unstable
165 * @category models
166 * @since 4.0.0
167 */
168export interface Initial<A, E = never> extends AsyncResult.Proto<A, E> {
169 readonly _tag: "Initial"
170}
171
172/**
173 * Converts an `Exit` into a `Success` when it succeeds or a `Failure` carrying the exit cause when it fails.
174 *
175 * @stability unstable
176 * @category constructors
177 * @since 4.0.0
178 */
179export const fromExit = <A, E>(exit: Exit.Exit<A, E>): Success<A, E> | Failure<A, E> =>
180 exit._tag === "Success" ? success(exit.value) : failure(exit.cause)
181
182/**
183 * Converts an `Exit` to a result, preserving the latest previous success when the exit is a failure.
184 *
185 * @stability unstable
186 * @category constructors
187 * @since 4.0.0
188 */
189export const fromExitWithPrevious = <A, E>(
190 exit: Exit.Exit<A, E>,
191 previous: Option.Option<AsyncResult<A, E>>
192): Success<A, E> | Failure<A, E> =>
193 exit._tag === "Success" ? success(exit.value) : failureWithPrevious(exit.cause, { previous })
194
195/**
196 * Creates a waiting result from an optional previous result, using `Initial(true)` when no previous result exists.
197 *
198 * @stability unstable
199 * @category constructors
200 * @since 4.0.0
201 */
202export const waitingFrom = <A, E>(previous: Option.Option<AsyncResult<A, E>>): AsyncResult<A, E> => {
203 if (previous._tag === "None") {
204 return initial(true)
205 }
206 return waiting(previous.value)
207}
208
209/**
210 * Returns `true` when an `AsyncResult` is in the `Initial` state.
211 *
212 * @stability unstable
213 * @category guards
214 * @since 4.0.0
215 */
216export const isInitial = <A, E>(result: AsyncResult<A, E>): result is Initial<A, E> => result._tag === "Initial"
217
218/**
219 * Returns `true` when an `AsyncResult` is either `Success` or `Failure`.
220 *
221 * @stability unstable
222 * @category guards
223 * @since 4.0.0
224 */
225export const isNotInitial = <A, E>(result: AsyncResult<A, E>): result is Success<A, E> | Failure<A, E> =>
226 result._tag !== "Initial"
227
228/**
229 * Creates an `Initial` result, optionally marking it as waiting.
230 *
231 * @stability unstable
232 * @category constructors
233 * @since 4.0.0
234 */
235export const initial = <A = never, E = never>(waiting = false): Initial<A, E> => {
236 const result = Object.create(ResultProto)
237 result._tag = "Initial"
238 result.waiting = waiting
239 return result
240}
241
242/**
243 * Successful `AsyncResult` containing the current value, its timestamp, and the shared waiting flag.
244 *
245 * @stability unstable
246 * @category models
247 * @since 4.0.0
248 */
249export interface Success<A, E = never> extends AsyncResult.Proto<A, E> {
250 readonly _tag: "Success"
251 readonly value: A
252 readonly timestamp: number
253}
254
255/**
256 * Returns `true` when an `AsyncResult` is a `Success`.
257 *
258 * @stability unstable
259 * @category guards
260 * @since 4.0.0
261 */
262export const isSuccess = <A, E>(result: AsyncResult<A, E>): result is Success<A, E> => result._tag === "Success"
263
264/**
265 * Creates a `Success` result with a value and optional `waiting` flag or timestamp override.
266 *
267 * @stability unstable
268 * @category constructors
269 * @since 4.0.0
270 */
271export const success = <A, E = never>(value: A, options?: {
272 readonly waiting?: boolean | undefined
273 readonly timestamp?: number | undefined
274}): Success<A, E> => {
275 const result = Object.create(ResultProto)
276 result._tag = "Success"
277 result.value = value
278 result.waiting = options?.waiting ?? false
279 result.timestamp = options?.timestamp ?? Date.now()
280 return result
281}
282
283/**
284 * Failed `AsyncResult` containing a failure cause and the latest previous success when one is available.
285 *
286 * @stability unstable
287 * @category models
288 * @since 4.0.0
289 */
290export interface Failure<A, E = never> extends AsyncResult.Proto<A, E> {
291 readonly _tag: "Failure"
292 readonly cause: Cause.Cause<E>
293 readonly previousSuccess: Option.Option<Success<A, E>>
294}
295
296/**
297 * Returns `true` when an `AsyncResult` is a `Failure`.
298 *
299 * @stability unstable
300 * @category guards
301 * @since 4.0.0
302 */
303export const isFailure = <A, E>(result: AsyncResult<A, E>): result is Failure<A, E> => result._tag === "Failure"
304
305/**
306 * Returns `true` when an `AsyncResult` is a `Failure` whose cause contains only interruptions.
307 *
308 * @stability unstable
309 * @category guards
310 * @since 4.0.0
311 */
312export const isInterrupted = <A, E>(result: AsyncResult<A, E>): result is Failure<A, E> =>
313 result._tag === "Failure" && Cause.hasInterruptsOnly(result.cause)
314
315/**
316 * Creates a `Failure` result from a `Cause`, optionally preserving a previous success and marking the result as waiting.
317 *
318 * @stability unstable
319 * @category constructors
320 * @since 4.0.0
321 */
322export const failure = <A, E = never>(
323 cause: Cause.Cause<E>,
324 options?: {
325 readonly previousSuccess?: Option.Option<Success<A, E>> | undefined
326 readonly waiting?: boolean | undefined
327 }
328): Failure<A, E> => {
329 const result = Object.create(ResultProto)
330 result._tag = "Failure"
331 result.cause = cause
332 result.previousSuccess = options?.previousSuccess ?? Option.none()
333 result.waiting = options?.waiting ?? false
334 return result
335}
336
337/**
338 * Creates a `Failure` result from a `Cause`, carrying forward the latest success stored in a previous result.
339 *
340 * @stability unstable
341 * @category constructors
342 * @since 4.0.0
343 */
344export const failureWithPrevious = <A, E>(
345 cause: Cause.Cause<E>,
346 options: {
347 readonly previous: Option.Option<AsyncResult<A, E>>
348 readonly waiting?: boolean | undefined
349 }
350): Failure<A, E> =>
351 failure(cause, {
352 previousSuccess: Option.flatMap(options.previous, (result) =>
353 isSuccess(result)
354 ? Option.some(result)
355 : isFailure(result)
356 ? result.previousSuccess
357 : Option.none()),
358 waiting: options.waiting
359 })
360
361/**
362 * Creates a `Failure` result from a typed error, wrapping it in `Cause.fail`.
363 *
364 * @stability unstable
365 * @category constructors
366 * @since 4.0.0
367 */
368export const fail = <E, A = never>(error: E, options?: {
369 readonly previousSuccess?: Option.Option<Success<A, E>> | undefined
370 readonly waiting?: boolean | undefined
371}): Failure<A, E> => failure(Cause.fail(error), options)
372
373/**
374 * Creates a `Failure` result from a typed error while carrying forward the latest success stored in a previous result.
375 *
376 * @stability unstable
377 * @category constructors
378 * @since 4.0.0
379 */
380export const failWithPrevious = <A, E>(
381 error: E,
382 options: {
383 readonly previous: Option.Option<AsyncResult<A, E>>
384 readonly waiting?: boolean | undefined
385 }
386): Failure<A, E> => failureWithPrevious(Cause.fail(error), options)
387
388/**
389 * Marks an `AsyncResult` as waiting, optionally touching the timestamp when the result is a `Success`.
390 *
391 * @stability unstable
392 * @category constructors
393 * @since 4.0.0
394 */
395export const waiting: {
396 (options?: { readonly touch?: boolean | undefined }): <R extends AsyncResult<any, any>>(self: R) => R
397 <R extends AsyncResult<any, any>>(self: R, options?: { readonly touch?: boolean | undefined }): R
398} = dual((args) => isAsyncResult(args[0]), <R extends AsyncResult<any, any>>(self: R, options?: {
399 readonly touch?: boolean | undefined
400}): R => {
401 if (self.waiting) {
402 return options?.touch ? touch(self) : self
403 }
404 const result = Object.assign(Object.create(ResultProto), self)
405 result.waiting = true
406 if (options?.touch && isSuccess(result)) {
407 ;(result as any).timestamp = Date.now()
408 }
409 return result
410})
411
412/**
413 * Refreshes the timestamp of a `Success` result while preserving its value and waiting flag; non-success results are returned unchanged.
414 *
415 * @stability unstable
416 * @category combinators
417 * @since 4.0.0
418 */
419export const touch = <A extends AsyncResult<any, any>>(result: A): A => {
420 if (isSuccess(result)) {
421 return success(result.value, { waiting: result.waiting }) as A
422 }
423 return result
424}
425
426/**
427 * Replaces a `Failure` value's stored previous success with the latest success
428 * found in another result.
429 *
430 * @stability unstable
431 * @category combinators
432 * @since 4.0.0
433 */
434export const replacePrevious: {
435 <XE, A>(previous: Option.Option<AsyncResult<A, XE>>): <R extends AsyncResult<any, any>>(
436 self: R
437 ) => With<R, A, AsyncResult.Failure<R>>
438 <R extends AsyncResult<any, any>, XE, A>(
439 self: R,
440 previous: Option.Option<AsyncResult<A, XE>>
441 ): With<R, A, AsyncResult.Failure<R>>
442} = dual(2, <R extends AsyncResult<any, any>, XE, A>(
443 self: R,
444 previous: Option.Option<AsyncResult<A, XE>>
445): With<R, A, AsyncResult.Failure<R>> => {
446 if (self._tag === "Failure") {
447 return failureWithPrevious(self.cause, { previous, waiting: self.waiting }) as any
448 }
449 return self as any
450})
451
452/**
453 * Returns the current success value, or the previous success value stored in a failure, as an `Option`.
454 *
455 * @stability unstable
456 * @category accessors
457 * @since 4.0.0
458 */
459export const value = <A, E>(self: AsyncResult<A, E>): Option.Option<A> => {
460 if (self._tag === "Success") {
461 return Option.some(self.value)
462 } else if (self._tag === "Failure") {
463 return Option.map(self.previousSuccess, (s) => s.value)
464 }
465 return Option.none()
466}
467
468/**
469 * Returns the available value from `value`, or evaluates the fallback when no current or previous success exists.
470 *
471 * @stability unstable
472 * @category accessors
473 * @since 4.0.0
474 */
475export const getOrElse: {
476 <B>(orElse: LazyArg<B>): <A, E>(self: AsyncResult<A, E>) => A | B
477 <A, E, B>(self: AsyncResult<A, E>, orElse: LazyArg<B>): A | B
478} = dual(2, <A, E, B>(self: AsyncResult<A, E>, orElse: LazyArg<B>): A | B => Option.getOrElse(value(self), orElse))
479
480/**
481 * Returns the available value from `value`, or throws `NoSuchElementError` when no current or previous success exists.
482 *
483 * @stability unstable
484 * @category accessors
485 * @since 4.0.0
486 */
487export const getOrThrow = <A, E>(self: AsyncResult<A, E>): A =>
488 Option.getOrThrowWith(value(self), () => new Cause.NoSuchElementError("AsyncResult.getOrThrow: no value found"))
489
490/**
491 * Returns the failure cause when the result is a `Failure`, otherwise `None`.
492 *
493 * @stability unstable
494 * @category accessors
495 * @since 4.0.0
496 */
497export const cause = <A, E>(self: AsyncResult<A, E>): Option.Option<Cause.Cause<E>> =>
498 self._tag === "Failure" ? Option.some(self.cause) : Option.none()
499
500/**
501 * Returns the first typed error from a failure cause, or `None` for successes, initial results, defects, and interrupt-only causes.
502 *
503 * @stability unstable
504 * @category accessors
505 * @since 4.0.0
506 */
507export const error = <A, E>(self: AsyncResult<A, E>): Option.Option<E> =>
508 self._tag === "Failure" ? Cause.findErrorOption(self.cause) : Option.none()
509
510/**
511 * Converts a result to an `Exit`, succeeding with a success value, failing with a failure cause, or failing with `NoSuchElementError` for `Initial`.
512 *
513 * @stability unstable
514 * @category combinators
515 * @since 4.0.0
516 */
517export const toExit = <A, E>(
518 self: AsyncResult<A, E>
519): Exit.Exit<A, E | Cause.NoSuchElementError> => {
520 switch (self._tag) {
521 case "Success": {
522 return Exit.succeed(self.value)
523 }
524 case "Failure": {
525 return Exit.failCause(self.cause)
526 }
527 default: {
528 return Exit.fail(new Cause.NoSuchElementError())
529 }
530 }
531}
532
533/**
534 * Maps the success value of an `AsyncResult`, also mapping any previous success stored in a failure while leaving initial results unchanged.
535 *
536 * @stability unstable
537 * @category combinators
538 * @since 4.0.0
539 */
540export const map: {
541 <A, B>(f: (a: A) => B): <E>(self: AsyncResult<A, E>) => AsyncResult<B, E>
542 <E, A, B>(self: AsyncResult<A, E>, f: (a: A) => B): AsyncResult<B, E>
543} = dual(2, <E, A, B>(self: AsyncResult<A, E>, f: (a: A) => B): AsyncResult<B, E> => {
544 switch (self._tag) {
545 case "Initial":
546 return self as any as AsyncResult<B, E>
547 case "Failure":
548 return failure(self.cause, {
549 previousSuccess: Option.map(self.previousSuccess, (s) => success(f(s.value), s)),
550 waiting: self.waiting
551 })
552 case "Success":
553 return success(f(self.value), self)
554 }
555})
556
557/**
558 * Maps the success value of an `AsyncResult` and flattens the result.
559 *
560 * **When to use**
561 *
562 * Use to sequence computations that may return another `AsyncResult` while
563 * preserving initial and failure states.
564 *
565 * **Details**
566 *
567 * Initial results are left unchanged. Failures preserve their cause and remap
568 * the stored previous success when the mapping function returns a success.
569 *
570 * @stability unstable
571 * @category combinators
572 * @since 4.0.0
573 */
574export const flatMap: {
575 <A, E, B, E2>(
576 f: (a: A, prev: Success<A, E>) => AsyncResult<A, E2>
577 ): (self: AsyncResult<A, E>) => AsyncResult<B, E | E2>
578 <E, A, B, E2>(self: AsyncResult<A, E>, f: (a: A, prev: Success<A, E>) => AsyncResult<B, E2>): AsyncResult<B, E | E2>
579} = dual(
580 2,
581 <E, A, B, E2>(
582 self: AsyncResult<A, E>,
583 f: (a: A, prev: Success<A, E>) => AsyncResult<B, E2>
584 ): AsyncResult<B, E | E2> => {
585 switch (self._tag) {
586 case "Initial":
587 return self as any as AsyncResult<B, E>
588 case "Failure":
589 return failure<B, E | E2>(self.cause, {
590 previousSuccess: Option.flatMap(self.previousSuccess, (s) => {
591 const next = f(s.value, s)
592 return isSuccess(next) ? Option.some(next) : Option.none()
593 }),
594 waiting: self.waiting
595 })
596 case "Success":
597 return f(self.value, self)
598 }
599 }
600)
601
602/**
603 * Pattern matches an `AsyncResult` by calling the handler for `Initial`, `Failure`, or `Success`.
604 *
605 * @stability unstable
606 * @category combinators
607 * @since 4.0.0
608 */
609export const match: {
610 <A, E, X, Y, Z>(options: {
611 readonly onInitial: (_: Initial<A, E>) => X
612 readonly onFailure: (_: Failure<A, E>) => Y
613 readonly onSuccess: (_: Success<A, E>) => Z
614 }): (self: AsyncResult<A, E>) => X | Y | Z
615 <A, E, X, Y, Z>(self: AsyncResult<A, E>, options: {
616 readonly onInitial: (_: Initial<A, E>) => X
617 readonly onFailure: (_: Failure<A, E>) => Y
618 readonly onSuccess: (_: Success<A, E>) => Z
619 }): X | Y | Z
620} = dual(2, <A, E, X, Y, Z>(self: AsyncResult<A, E>, options: {
621 readonly onInitial: (_: Initial<A, E>) => X
622 readonly onFailure: (_: Failure<A, E>) => Y
623 readonly onSuccess: (_: Success<A, E>) => Z
624}): X | Y | Z => {
625 switch (self._tag) {
626 case "Initial":
627 return options.onInitial(self)
628 case "Failure":
629 return options.onFailure(self)
630 case "Success":
631 return options.onSuccess(self)
632 }
633})
634
635/**
636 * Pattern matches a result, handling successes and initials directly while splitting failures into typed errors or squashed non-error causes passed to `onDefect`.
637 *
638 * @stability unstable
639 * @category combinators
640 * @since 4.0.0
641 */
642export const matchWithError: {
643 <A, E, W, X, Y, Z>(options: {
644 readonly onInitial: (_: Initial<A, E>) => W
645 readonly onError: (error: E, _: Failure<A, E>) => X
646 readonly onDefect: (defect: unknown, _: Failure<A, E>) => Y
647 readonly onSuccess: (_: Success<A, E>) => Z
648 }): (self: AsyncResult<A, E>) => W | X | Y | Z
649 <A, E, W, X, Y, Z>(self: AsyncResult<A, E>, options: {
650 readonly onInitial: (_: Initial<A, E>) => W
651 readonly onError: (error: E, _: Failure<A, E>) => X
652 readonly onDefect: (defect: unknown, _: Failure<A, E>) => Y
653 readonly onSuccess: (_: Success<A, E>) => Z
654 }): W | X | Y | Z
655} = dual(2, <A, E, W, X, Y, Z>(self: AsyncResult<A, E>, options: {
656 readonly onInitial: (_: Initial<A, E>) => W
657 readonly onError: (error: E, _: Failure<A, E>) => X
658 readonly onDefect: (defect: unknown, _: Failure<A, E>) => Y
659 readonly onSuccess: (_: Success<A, E>) => Z
660}): W | X | Y | Z => {
661 switch (self._tag) {
662 case "Initial":
663 return options.onInitial(self)
664 case "Failure": {
665 const result = Cause.findError(self.cause)
666 if (Result.isFailure(result)) {
667 return options.onDefect(Cause.squash(result.failure), self)
668 }
669 return options.onError(result.success, self)
670 }
671 case "Success":
672 return options.onSuccess(self)
673 }
674})
675
676/**
677 * Pattern matches a result by calling `onWaiting` for waiting or initial states, otherwise handling successes and splitting failures into typed errors or squashed non-error causes.
678 *
679 * @stability unstable
680 * @category combinators
681 * @since 4.0.0
682 */
683export const matchWithWaiting: {
684 <A, E, W, X, Y, Z>(options: {
685 readonly onWaiting: (_: AsyncResult<A, E>) => W
686 readonly onError: (error: E, _: Failure<A, E>) => X
687 readonly onDefect: (defect: unknown, _: Failure<A, E>) => Y
688 readonly onSuccess: (_: Success<A, E>) => Z
689 }): (self: AsyncResult<A, E>) => W | X | Y | Z
690 <A, E, W, X, Y, Z>(self: AsyncResult<A, E>, options: {
691 readonly onWaiting: (_: AsyncResult<A, E>) => W
692 readonly onError: (error: E, _: Failure<A, E>) => X
693 readonly onDefect: (defect: unknown, _: Failure<A, E>) => Y
694 readonly onSuccess: (_: Success<A, E>) => Z
695 }): W | X | Y | Z
696} = dual(2, <A, E, W, X, Y, Z>(self: AsyncResult<A, E>, options: {
697 readonly onWaiting: (_: AsyncResult<A, E>) => W
698 readonly onError: (error: E, _: Failure<A, E>) => X
699 readonly onDefect: (defect: unknown, _: Failure<A, E>) => Y
700 readonly onSuccess: (_: Success<A, E>) => Z
701}): W | X | Y | Z => {
702 if (self.waiting) {
703 return options.onWaiting(self)
704 }
705 switch (self._tag) {
706 case "Initial":
707 return options.onWaiting(self)
708 case "Failure": {
709 const e = Cause.findError(self.cause)
710 if (Result.isFailure(e)) {
711 return options.onDefect(Cause.squash(e.failure), self)
712 }
713 return options.onError(e.success, self)
714 }
715 case "Success":
716 return options.onSuccess(self)
717 }
718})
719
720/**
721 * Combines an iterable or record of `AsyncResult` and plain values into one `AsyncResult`, returning the first non-success result or a success of the collected values marked waiting when any input success is waiting.
722 *
723 * @stability unstable
724 * @category combinators
725 * @since 4.0.0
726 */
727export const all = <const Arg extends Iterable<any> | Record<string, any>>(
728 results: Arg
729): AsyncResult<
730 [Arg] extends [ReadonlyArray<any>] ? {
731 -readonly [K in keyof Arg]: [Arg[K]] extends [AsyncResult<infer _A, infer _E>] ? _A : Arg[K]
732 }
733 : [Arg] extends [Iterable<infer _A>] ? _A extends AsyncResult<infer _AA, infer _E> ? _AA : _A
734 : [Arg] extends [Record<string, any>] ? {
735 -readonly [K in keyof Arg]: [Arg[K]] extends [AsyncResult<infer _A, infer _E>] ? _A : Arg[K]
736 }
737 : never,
738 [Arg] extends [ReadonlyArray<any>] ? AsyncResult.Failure<Arg[number]>
739 : [Arg] extends [Iterable<infer _A>] ? AsyncResult.Failure<_A>
740 : [Arg] extends [Record<string, any>] ? AsyncResult.Failure<Arg[keyof Arg]>
741 : never
742> => {
743 const isIter = isIterable(results)
744 const entries = isIter
745 ? Array.from(results, (result, i) => [i, result] as const)
746 : Object.entries(results)
747 const successes: any = isIter ? [] : {}
748 let waiting = false
749 for (let i = 0; i < entries.length; i++) {
750 const [key, result] = entries[i]
751 if (!isAsyncResult(result)) {
752 InternalRecord.assignProperty(successes, key, result)
753 continue
754 } else if (!isSuccess(result)) {
755 return result as any
756 }
757 InternalRecord.assignProperty(successes, key, result.value)
758 if (result.waiting) {
759 waiting = true
760 }
761 }
762 return success(successes, { waiting }) as any
763}
764
765/**
766 * Creates a typed builder for rendering an `AsyncResult` by handling waiting, initial, success, error, defect, interrupt, and failure cases.
767 *
768 * @stability unstable
769 * @category constructors
770 * @since 4.0.0
771 */
772export const builder = <A extends AsyncResult<any, any>>(self: A): Builder<
773 never,
774 A extends Success<infer _A, infer _E> ? _A : never,
775 A extends Failure<infer _A, infer _E> ? _E : never,
776 A extends Initial<infer _A, infer _E> ? true : never,
777 A extends Failure<infer _A, infer _E> ? Defect | Interrupt : never
778> => new BuilderImpl(self) as any
779
780/**
781 * Type marker used by `Builder` to track whether defect failures still need to be handled.
782 *
783 * @stability unstable
784 * @category utility types
785 * @since 4.0.0
786 */
787export interface Defect {
788 readonly _: unique symbol
789}
790
791/**
792 * Type marker used by `Builder` to track whether interrupt failures still need to be handled.
793 *
794 * @stability unstable
795 * @category utility types
796 * @since 4.0.0
797 */
798export interface Interrupt {
799 readonly _: unique symbol
800}
801
802/**
803 * Fluent renderer for `AsyncResult` values that tracks unhandled cases at the type level and exposes `exhaustive` only after all possible cases are handled.
804 *
805 * @stability unstable
806 * @category models
807 * @since 4.0.0
808 */
809export type Builder<Out, A, E, I, F> =
810 & Pipeable
811 & {
812 onWaiting<B>(f: (result: AsyncResult<A, E>) => B): Builder<Out | B, A, E, I, F>
813 orElse<B>(orElse: LazyArg<B>): Out | B
814 orNull(): Out | null
815 render(): [A | I] extends [never] ? Out : Out | null
816 }
817 & ([A | E | I | F] extends [never] ? {
818 exhaustive(): Out
819 } :
820 unknown)
821 & ([I] extends [never] ? unknown :
822 {
823 onInitial<B>(f: (result: Initial<A, E>) => B): Builder<Out | B, A, E, never, F>
824 onInitialOrWaiting<B>(f: (result: AsyncResult<A, E>) => B): Builder<Out | B, A, E, never, F>
825 })
826 & ([A] extends [never] ? unknown :
827 {
828 onSuccess<B>(f: (value: A, result: Success<A, E>) => B): Builder<Out | B, never, E, I, F>
829 })
830 & ([E] extends [never] ? unknown : {
831 onError<B>(f: (error: E, result: Failure<A, E>) => B): Builder<Out | B, A, never, I, F>
832
833 onErrorIf<B extends E, C>(
834 refinement: Refinement<E, B>,
835 f: (error: B, result: Failure<A, E>) => C
836 ): Builder<Out | C, A, Types.EqualsWith<E, B, E, Exclude<E, B>>, I, F>
837 onErrorIf<C>(
838 predicate: Predicate<E>,
839 f: (error: E, result: Failure<A, E>) => C
840 ): Builder<Out | C, A, E, I, F>
841
842 onErrorTag<const Tags extends ReadonlyArray<Types.Tags<E>>, B>(
843 tags: Tags,
844 f: (error: Types.ExtractTag<E, Tags[number]>, result: Failure<A, E>) => B
845 ): Builder<Out | B, A, Types.ExcludeTag<E, Tags[number]>, I, F>
846 onErrorTag<const Tag extends Types.Tags<E>, B>(
847 tag: Tag,
848 f: (error: Types.ExtractTag<E, Tag>, result: Failure<A, E>) => B
849 ): Builder<Out | B, A, Types.ExcludeTag<E, Tag>, I, F>
850 })
851 & ([E | F] extends [never] ? unknown : {
852 onFailure<B>(f: (cause: Cause.Cause<E>, result: Failure<A, E>) => B): Builder<Out | B, A, never, I, never>
853 })
854 & (Interrupt extends F ? {
855 onInterrupt<B>(
856 f: (interruptors: ReadonlySet<number>, result: Failure<A, E>) => B
857 ): Builder<Out | B, A, E, I, Exclude<F, Interrupt>>
858 } :
859 unknown)
860 & (Defect extends F ? {
861 onDefect<B>(f: (defect: unknown, result: Failure<A, E>) => B): Builder<Out | B, A, E, I, Exclude<F, Defect>>
862 } :
863 unknown)
864
865class BuilderImpl<Out, A, E> {
866 constructor(result: AsyncResult<A, E>) {
867 this.result = result
868 }
869 readonly result: AsyncResult<A, E>
870 public output = Option.none<Out>()
871
872 when<B extends AsyncResult<A, E>, C>(
873 refinement: Refinement<AsyncResult<A, E>, B>,
874 f: (result: B) => Option.Option<C>
875 ): any
876 when<C>(
877 refinement: Predicate<AsyncResult<A, E>>,
878 f: (result: AsyncResult<A, E>) => Option.Option<C>
879 ): any
880 when<C>(
881 refinement: Predicate<AsyncResult<A, E>>,
882 f: (result: AsyncResult<A, E>) => Option.Option<C>
883 ): any {
884 if (Option.isNone(this.output) && refinement(this.result)) {
885 const b = f(this.result)
886 if (Option.isSome(b)) {
887 ;(this as any).output = b
888 }
889 }
890 return this
891 }
892
893 pipe() {
894 return pipeArguments(this, arguments)
895 }
896
897 onWaiting<B>(f: (result: AsyncResult<A, E>) => B): BuilderImpl<Out | B, A, E> {
898 return this.when((r) => r.waiting, (r) => Option.some(f(r)))
899 }
900
901 onInitialOrWaiting<B>(f: (result: AsyncResult<A, E>) => B): BuilderImpl<Out | B, A, E> {
902 return this.when((r) => isInitial(r) || r.waiting, (r) => Option.some(f(r)))
903 }
904
905 onInitial<B>(f: (result: Initial<A, E>) => B): BuilderImpl<Out | B, A, E> {
906 return this.when(isInitial, (r) => Option.some(f(r)))
907 }
908
909 onSuccess<B>(f: (value: A, result: Success<A, E>) => B): BuilderImpl<Out | B, never, E> {
910 return this.when(isSuccess, (r) => Option.some(f(r.value, r)))
911 }
912
913 onFailure<B>(f: (cause: Cause.Cause<E>, result: Failure<A, E>) => B): BuilderImpl<Out | B, A, never> {
914 return this.when(isFailure, (r) => Option.some(f(r.cause, r)))
915 }
916
917 onError<B>(f: (error: E, result: Failure<A, E>) => B): BuilderImpl<Out | B, A, never> {
918 return this.onErrorIf(constTrue, f) as any
919 }
920
921 onErrorIf<C, B extends E = E>(
922 refinement: Refinement<E, B> | Predicate<E>,
923 f: (error: B, result: Failure<A, E>) => C
924 ): BuilderImpl<Out | C, A, Types.EqualsWith<E, B, E, Exclude<E, B>>> {
925 return this.when(isFailure, (result) =>
926 Cause.findErrorOption(result.cause).pipe(
927 Option.filter(refinement),
928 Option.map((error) => f(error as B, result))
929 ))
930 }
931
932 onErrorTag<B>(
933 tag: string | ReadonlyArray<string>,
934 f: (error: Types.ExtractTag<E, any>, result: Failure<A, E>) => B
935 ): BuilderImpl<Out | B, A, Types.ExcludeTag<E, any>> {
936 return this.onErrorIf(
937 (e) => hasProperty(e, "_tag") && (Array.isArray(tag) ? tag.includes(e._tag) : e._tag === tag),
938 f
939 ) as any
940 }
941
942 onDefect<B>(f: (defect: unknown, result: Failure<A, E>) => B): BuilderImpl<Out | B, A, E> {
943 return this.when(isFailure, (result) => {
944 const defect = Cause.findDefect(result.cause)
945 return Result.isFailure(defect) ? Option.none() : Option.some(f(defect.success, result))
946 })
947 }
948
949 onInterrupt<B>(f: (interruptors: ReadonlySet<number>, result: Failure<A, E>) => B): BuilderImpl<Out | B, A, E> {
950 return this.when(isFailure, (result) => {
951 const interruptors = Cause.filterInterruptors(result.cause)
952 return Result.isFailure(interruptors) ? Option.none() : Option.some(f(interruptors.success, result))
953 })
954 }
955
956 orElse<B>(orElse: LazyArg<B>): Out | B {
957 return Option.getOrElse(this.output, orElse)
958 }
959
960 orNull(): Out | null {
961 return Option.getOrNull(this.output)
962 }
963
964 render(): Out | null {
965 if (Option.isSome(this.output)) {
966 return this.output.value
967 } else if (isFailure(this.result)) {
968 throw Cause.squash(this.result.cause)
969 }
970 return null
971 }
972
973 exhaustive(): Out {
974 return this.render() as Out
975 }
976}
977
978/**
979 * Schema interface for `AsyncResult` values, retaining the schemas used for success values and failure errors.
980 *
981 * @stability unstable
982 * @category schemas
983 * @since 4.0.0
984 */
985export interface Schema<
986 Success extends Schema_.Constraint,
987 Error extends Schema_.Constraint
988> extends
989 Schema_.declareConstructor<
990 AsyncResult<Success["Type"], Error["Type"]>,
991 AsyncResult<Success["Encoded"], Error["Encoded"]>,
992 readonly [Success, Schema_.Cause<Error, Schema_.Defect>]
993 >
994{
995 readonly success: Success
996 readonly error: Error
997}
998
999/**
1000 * Creates a schema for `AsyncResult` values using optional schemas for success values and failure errors.
1001 *
1002 * @stability unstable
1003 * @category schemas
1004 * @since 4.0.0
1005 */
1006export const Schema = <
1007 A extends Schema_.Constraint = Schema_.Never,
1008 E extends Schema_.Constraint = Schema_.Never
1009>(
1010 options: {
1011 readonly success?: A | undefined
1012 readonly error?: E | undefined
1013 }
1014): Schema<A, E> => {
1015 const success_: A = options.success ?? Schema_.Never as any
1016 const error: E = options.error ?? Schema_.Never as any
1017 const schema = Schema_.declareConstructor<
1018 AsyncResult<A["Type"], E["Type"]>,
1019 AsyncResult<A["Encoded"], E["Encoded"]>
1020 >()(
1021 [success_, Schema_.Cause(error, Schema_.Defect())],
1022 ([value, cause]) => (input, ast, options) => {
1023 if (!isAsyncResult(input)) {
1024 return Effect.fail(new SchemaIssue.InvalidType(ast, input, options))
1025 }
1026 switch (input._tag) {
1027 case "Initial":
1028 return Effect.succeed(input)
1029 case "Success":
1030 return Effect.mapBothEager(
1031 SchemaParser.decodeUnknownEffect(value)(input.value, options),
1032 {
1033 onSuccess: (value) => success(value, input),
1034 onFailure: (issue) => SchemaIssue.makeCompositeAtKey(ast, "value", issue, input, options)
1035 }
1036 )
1037 case "Failure": {
1038 const prevSuccessEffect = input.previousSuccess.pipe(
1039 Option.map((ps) =>
1040 Effect.mapBothEager(
1041 SchemaParser.decodeUnknownEffect(value)(ps.value, options),
1042 {
1043 onSuccess: (value) => Option.some(success<A["Type"], E["Type"]>(value, ps)),
1044 onFailure: (issue) =>
1045 new SchemaIssue.Composite(
1046 ast,
1047 [
1048 new SchemaIssue.Pointer(["previousSuccess", "value"], issue)
1049 ],
1050 input,
1051 options
1052 )
1053 }
1054 )
1055 ),
1056 Option.getOrElse(() => Effect.succeedNone)
1057 )
1058 const causeEffect = Effect.mapErrorEager(
1059 SchemaParser.decodeUnknownEffect(cause)(input.cause, options),
1060 (issue) => SchemaIssue.makeCompositeAtKey(ast, "cause", issue, input, options)
1061 )
1062 return Effect.flatMapEager(
1063 prevSuccessEffect,
1064 (previousSuccess) =>
1065 Effect.mapEager(causeEffect, (cause) =>
1066 failure(cause, {
1067 previousSuccess,
1068 waiting: input.waiting
1069 }))
1070 )
1071 }
1072 }
1073 },
1074 {
1075 expected: "AsyncResult",
1076 toCodec([value, cause]) {
1077 const Success = Schema_.TaggedStruct("Success", { value, waiting: Schema_.Boolean, timestamp: Schema_.Int })
1078 return Schema_.link<AsyncResult<A["Encoded"], E["Encoded"]>>()(
1079 Schema_.Union([
1080 Schema_.TaggedStruct("Initial", { waiting: Schema_.Boolean }),
1081 Success,
1082 Schema_.TaggedStruct("Failure", {
1083 cause,
1084 previousSuccess: Schema_.Option(Success),
1085 waiting: Schema_.Boolean
1086 })
1087 ]),
1088 SchemaTransformation.transform({
1089 decode: (encoded): AsyncResult<A["Encoded"], E["Encoded"]> => {
1090 switch (encoded._tag) {
1091 case "Initial":
1092 return initial(encoded.waiting)
1093 case "Success":
1094 return success(encoded.value, {
1095 waiting: encoded.waiting,
1096 timestamp: encoded.timestamp
1097 })
1098 case "Failure": {
1099 return failure(encoded.cause, {
1100 previousSuccess: Option.map(encoded.previousSuccess, (ps) => success(ps.value, ps)),
1101 waiting: encoded.waiting
1102 })
1103 }
1104 }
1105 },
1106 encode(result) {
1107 switch (result._tag) {
1108 case "Initial":
1109 return { _tag: "Initial" as const, waiting: result.waiting }
1110 case "Success":
1111 return {
1112 _tag: "Success" as const,
1113 value: result.value,
1114 waiting: result.waiting,
1115 timestamp: result.timestamp
1116 }
1117 case "Failure":
1118 return {
1119 _tag: "Failure" as const,
1120 cause: result.cause,
1121 previousSuccess: result.previousSuccess,
1122 waiting: result.waiting
1123 }
1124 }
1125 }
1126 })
1127 )
1128 },
1129 toEquivalence: Equal.asEquivalence,
1130 toFormatter: ([value, cause]) => (t) => {
1131 switch (t._tag) {
1132 case "Success":
1133 return `AsyncResult.Success(${value(t.value)}, ${t.waiting}, ${t.timestamp})`
1134 case "Failure":
1135 return `AsyncResult.Failure(${cause(t.cause)}, ${t.waiting})`
1136 case "Initial":
1137 return `AsyncResult.Initial(${t.waiting}, ${t.waiting})`
1138 }
1139 }
1140 }
1141 )
1142 return Object.assign(schema, {
1143 success: success_,
1144 error
1145 })
1146}