@fgv/ts-utils 5.1.0-11 → 5.1.0-12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,6 +56,27 @@ function processData(input: string): Result<ProcessedData> {
56
56
  }
57
57
  ```
58
58
 
59
+ ### Async Chaining
60
+
61
+ Use `thenOnSuccess()` to chain async operations without breaking the fluent style:
62
+
63
+ ```typescript
64
+ import { Result, succeed, captureAsyncResult } from '@fgv/ts-utils';
65
+
66
+ async function processData(input: string): Promise<Result<SavedData>> {
67
+ return parseInput(input)
68
+ .onSuccess((parsed) => validate(parsed))
69
+ .thenOnSuccess(async (valid) => fetchRelatedData(valid.id))
70
+ .onSuccess((data) => transform(data))
71
+ .thenOnSuccess(async (transformed) => saveData(transformed))
72
+ .withErrorFormat((msg) => `pipeline failed: ${msg}`);
73
+ }
74
+ ```
75
+
76
+ `AsyncResult<T>` implements `PromiseLike`, so the entire chain can be directly `await`ed. Rejected promises are caught and converted to `Failure`.
77
+
78
+ [Full async chaining documentation &rarr;](./src/packlets/base/README.md#async-result-chaining)
79
+
59
80
  ### Error Context
60
81
 
61
82
  ```typescript
@@ -101,6 +101,26 @@ export class Success {
101
101
  onFailure(__) {
102
102
  return this;
103
103
  }
104
+ /**
105
+ * {@inheritDoc IResult.thenOnSuccess}
106
+ */
107
+ thenOnSuccess(cb) {
108
+ try {
109
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
110
+ return new AsyncResult(cb(this._value));
111
+ }
112
+ catch (err) {
113
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
114
+ return AsyncResult.from(fail(_errorMessage(err)));
115
+ }
116
+ }
117
+ /**
118
+ * {@inheritDoc IResult.thenOnFailure}
119
+ */
120
+ thenOnFailure(__) {
121
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
122
+ return AsyncResult.from(this);
123
+ }
104
124
  /**
105
125
  * {@inheritDoc IResult.withErrorFormat}
106
126
  */
@@ -226,6 +246,26 @@ export class Failure {
226
246
  onFailure(cb) {
227
247
  return cb(this._message);
228
248
  }
249
+ /**
250
+ * {@inheritDoc IResult.thenOnSuccess}
251
+ */
252
+ thenOnSuccess(__) {
253
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
254
+ return AsyncResult.from(fail(this._message));
255
+ }
256
+ /**
257
+ * {@inheritDoc IResult.thenOnFailure}
258
+ */
259
+ thenOnFailure(cb) {
260
+ try {
261
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
262
+ return new AsyncResult(cb(this._message));
263
+ }
264
+ catch (err) {
265
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
266
+ return AsyncResult.from(fail(_errorMessage(err)));
267
+ }
268
+ }
229
269
  /**
230
270
  * {@inheritDoc IResult.withErrorFormat}
231
271
  */
@@ -591,13 +631,25 @@ export function propagateWithDetail(result, detail, successDetail) {
591
631
  ? succeedWithDetail(result.value, successDetail !== null && successDetail !== void 0 ? successDetail : detail)
592
632
  : failWithDetail(result.message, detail);
593
633
  }
634
+ /**
635
+ * Extracts a message string from an unknown thrown/rejected value.
636
+ * @param err - The caught error value.
637
+ * @returns The error message string.
638
+ * @internal
639
+ */
640
+ export function _errorMessage(err) {
641
+ if (err instanceof Error) {
642
+ return err.message;
643
+ }
644
+ return String(err);
645
+ }
594
646
  /**
595
647
  * Wraps a function which might throw to convert exception results
596
648
  * to {@link Failure}.
597
649
  * @param func - The function to be captured.
598
650
  * @returns Returns {@link Success} with a value of type `<T>` on
599
651
  * success , or {@link Failure} with the thrown error message if
600
- * `func` throws an `Error`.
652
+ * `func` throws an `Error` or string.
601
653
  * @public
602
654
  */
603
655
  export function captureResult(func) {
@@ -605,7 +657,180 @@ export function captureResult(func) {
605
657
  return succeed(func());
606
658
  }
607
659
  catch (err) {
608
- return fail(err.message);
660
+ return fail(_errorMessage(err));
661
+ }
662
+ }
663
+ /**
664
+ * Wraps a `Promise` of a {@link Result} to enable fluent chaining of both
665
+ * synchronous and asynchronous operations.
666
+ *
667
+ * @remarks
668
+ * `AsyncResult<T>` implements `PromiseLike` so it can be directly `await`ed.
669
+ * Use the `thenOnSuccess` and `thenOnFailure` methods on {@link Result} to bridge
670
+ * from synchronous to asynchronous result chains.
671
+ *
672
+ * @example
673
+ * ```typescript
674
+ * const result: Result<Final> = await parseInput(input)
675
+ * .thenOnSuccess(async (parsed) => fetchData(parsed))
676
+ * .onSuccess((data) => transform(data))
677
+ * .thenOnSuccess(async (transformed) => saveData(transformed))
678
+ * .withErrorFormat((msg) => `pipeline failed: ${msg}`);
679
+ * ```
680
+ *
681
+ * @public
682
+ */
683
+ export class AsyncResult {
684
+ /**
685
+ * Constructs an {@link AsyncResult} wrapping the supplied promise.
686
+ * @remarks
687
+ * If the supplied promise rejects, the rejection is caught and converted
688
+ * to a {@link Failure}, ensuring that awaiting an {@link AsyncResult} always
689
+ * yields a {@link Result}.
690
+ * @param promise - A `Promise` that resolves to a {@link Result}.
691
+ */
692
+ constructor(promise) {
693
+ this._promise = promise.catch((err) => fail(_errorMessage(err)));
694
+ }
695
+ /**
696
+ * Calls a supplied {@link SuccessContinuation | success continuation} if
697
+ * the wrapped result is successful.
698
+ * @param cb - The synchronous {@link SuccessContinuation | success continuation}
699
+ * to be called in the event of success.
700
+ * @returns A new {@link AsyncResult} wrapping the continuation result.
701
+ */
702
+ onSuccess(cb) {
703
+ return new AsyncResult(this._promise.then((r) => r.onSuccess(cb)));
704
+ }
705
+ /**
706
+ * Calls a supplied {@link AsyncSuccessContinuation | async success continuation} if
707
+ * the wrapped result is successful.
708
+ * @remarks
709
+ * Both synchronous throws and async rejections from the callback are caught
710
+ * and converted to a {@link Failure}.
711
+ * @param cb - The {@link AsyncSuccessContinuation | async success continuation}
712
+ * to be called in the event of success.
713
+ * @returns A new {@link AsyncResult} wrapping the async continuation result.
714
+ */
715
+ thenOnSuccess(cb) {
716
+ return new AsyncResult(this._promise.then(async (r) => {
717
+ if (r.isFailure()) {
718
+ return fail(r.message);
719
+ }
720
+ try {
721
+ return await cb(r.value);
722
+ }
723
+ catch (err) {
724
+ return fail(_errorMessage(err));
725
+ }
726
+ }));
727
+ }
728
+ /**
729
+ * Calls a supplied {@link FailureContinuation | failure continuation} if
730
+ * the wrapped result is a failure.
731
+ * @param cb - The synchronous {@link FailureContinuation | failure continuation}
732
+ * to be called in the event of failure.
733
+ * @returns A new {@link AsyncResult} wrapping the continuation result.
734
+ */
735
+ onFailure(cb) {
736
+ return new AsyncResult(this._promise.then((r) => r.onFailure(cb)));
737
+ }
738
+ /**
739
+ * Calls a supplied {@link AsyncFailureContinuation | async failure continuation} if
740
+ * the wrapped result is a failure.
741
+ * @remarks
742
+ * Both synchronous throws and async rejections from the callback are caught
743
+ * and converted to a {@link Failure}.
744
+ * @param cb - The {@link AsyncFailureContinuation | async failure continuation}
745
+ * to be called in the event of failure.
746
+ * @returns A new {@link AsyncResult} wrapping the async continuation result.
747
+ */
748
+ thenOnFailure(cb) {
749
+ return new AsyncResult(this._promise.then(async (r) => {
750
+ if (r.isSuccess()) {
751
+ return r;
752
+ }
753
+ try {
754
+ return await cb(r.message);
755
+ }
756
+ catch (err) {
757
+ return fail(_errorMessage(err));
758
+ }
759
+ }));
760
+ }
761
+ /**
762
+ * Calls a supplied {@link ErrorFormatter | error formatter} if
763
+ * the wrapped result is a failure.
764
+ * @param cb - The {@link ErrorFormatter | error formatter} to
765
+ * be called in the event of failure.
766
+ * @returns A new {@link AsyncResult} with the formatted error message,
767
+ * or the original success result.
768
+ */
769
+ withErrorFormat(cb) {
770
+ return new AsyncResult(this._promise.then((r) => r.withErrorFormat(cb)));
771
+ }
772
+ /**
773
+ * Propagates the wrapped result, appending any error message to the
774
+ * supplied errors aggregator.
775
+ * @param errors - {@link IMessageAggregator | Error aggregator} in which
776
+ * errors will be aggregated.
777
+ * @param formatter - An optional {@link ErrorFormatter | error formatter}
778
+ * to be used to format the error message.
779
+ * @returns A new {@link AsyncResult} wrapping the result after aggregation.
780
+ */
781
+ aggregateError(errors, formatter) {
782
+ return new AsyncResult(this._promise.then((r) => {
783
+ r.aggregateError(errors, formatter);
784
+ return r;
785
+ }));
786
+ }
787
+ /**
788
+ * Reports the wrapped result to the supplied reporter.
789
+ * @param reporter - The {@link IResultReporter | reporter} to which the result
790
+ * will be reported.
791
+ * @param options - The {@link IResultReportOptions | options} for reporting the result.
792
+ * @returns A new {@link AsyncResult} wrapping the result after reporting.
793
+ */
794
+ report(reporter, options) {
795
+ return new AsyncResult(this._promise.then((r) => {
796
+ r.report(reporter, options);
797
+ return r;
798
+ }));
799
+ }
800
+ /**
801
+ * Implementation of `PromiseLike.then` enabling `await` on {@link AsyncResult}.
802
+ * @param onfulfilled - Callback invoked when the promise resolves.
803
+ * @param onrejected - Callback invoked when the promise rejects.
804
+ * @returns A `Promise` resolving to the callback result.
805
+ */
806
+ /* eslint-disable @rushstack/no-new-null */
807
+ then(onfulfilled, onrejected) {
808
+ /* eslint-enable @rushstack/no-new-null */
809
+ return this._promise.then(onfulfilled, onrejected);
810
+ }
811
+ /**
812
+ * Creates an {@link AsyncResult} from a {@link Result}.
813
+ * @param result - The {@link Result} to wrap.
814
+ * @returns A new {@link AsyncResult} wrapping the supplied result.
815
+ */
816
+ static from(result) {
817
+ return new AsyncResult(Promise.resolve(result));
818
+ }
819
+ }
820
+ /**
821
+ * Wraps an async function which might throw to convert exception results
822
+ * to {@link Failure}.
823
+ * @param func - The async function to be captured.
824
+ * @returns Returns a `Promise` of {@link Success} with a value of type `<T>` on
825
+ * success, or {@link Failure} with the thrown error message if `func` throws or rejects.
826
+ * @public
827
+ */
828
+ export async function captureAsyncResult(func) {
829
+ try {
830
+ return succeed(await func());
831
+ }
832
+ catch (err) {
833
+ return fail(_errorMessage(err));
609
834
  }
610
835
  }
611
836
  //# sourceMappingURL=result.js.map
@@ -420,6 +420,131 @@ declare interface ArrayValidatorConstructorParams<T, TC = unknown> extends Valid
420
420
  */
421
421
  declare function asValidator<T, TC = unknown>(converterOrValidator: Converter<T, TC> | Validator<T, TC>): Validator<T, TC>;
422
422
 
423
+ /**
424
+ * Async continuation callback to be called in the event that a
425
+ * {@link Result} fails, returning a `Promise` of a new {@link Result}.
426
+ * @public
427
+ */
428
+ export declare type AsyncFailureContinuation<T> = (message: string) => Promise<Result<T>>;
429
+
430
+ /**
431
+ * Wraps a `Promise` of a {@link Result} to enable fluent chaining of both
432
+ * synchronous and asynchronous operations.
433
+ *
434
+ * @remarks
435
+ * `AsyncResult<T>` implements `PromiseLike` so it can be directly `await`ed.
436
+ * Use the `thenOnSuccess` and `thenOnFailure` methods on {@link Result} to bridge
437
+ * from synchronous to asynchronous result chains.
438
+ *
439
+ * @example
440
+ * ```typescript
441
+ * const result: Result<Final> = await parseInput(input)
442
+ * .thenOnSuccess(async (parsed) => fetchData(parsed))
443
+ * .onSuccess((data) => transform(data))
444
+ * .thenOnSuccess(async (transformed) => saveData(transformed))
445
+ * .withErrorFormat((msg) => `pipeline failed: ${msg}`);
446
+ * ```
447
+ *
448
+ * @public
449
+ */
450
+ export declare class AsyncResult<T> implements PromiseLike<Result<T>> {
451
+ private readonly _promise;
452
+ /**
453
+ * Constructs an {@link AsyncResult} wrapping the supplied promise.
454
+ * @remarks
455
+ * If the supplied promise rejects, the rejection is caught and converted
456
+ * to a {@link Failure}, ensuring that awaiting an {@link AsyncResult} always
457
+ * yields a {@link Result}.
458
+ * @param promise - A `Promise` that resolves to a {@link Result}.
459
+ */
460
+ constructor(promise: Promise<Result<T>>);
461
+ /**
462
+ * Calls a supplied {@link SuccessContinuation | success continuation} if
463
+ * the wrapped result is successful.
464
+ * @param cb - The synchronous {@link SuccessContinuation | success continuation}
465
+ * to be called in the event of success.
466
+ * @returns A new {@link AsyncResult} wrapping the continuation result.
467
+ */
468
+ onSuccess<TN>(cb: SuccessContinuation<T, TN>): AsyncResult<TN>;
469
+ /**
470
+ * Calls a supplied {@link AsyncSuccessContinuation | async success continuation} if
471
+ * the wrapped result is successful.
472
+ * @remarks
473
+ * Both synchronous throws and async rejections from the callback are caught
474
+ * and converted to a {@link Failure}.
475
+ * @param cb - The {@link AsyncSuccessContinuation | async success continuation}
476
+ * to be called in the event of success.
477
+ * @returns A new {@link AsyncResult} wrapping the async continuation result.
478
+ */
479
+ thenOnSuccess<TN>(cb: AsyncSuccessContinuation<T, TN>): AsyncResult<TN>;
480
+ /**
481
+ * Calls a supplied {@link FailureContinuation | failure continuation} if
482
+ * the wrapped result is a failure.
483
+ * @param cb - The synchronous {@link FailureContinuation | failure continuation}
484
+ * to be called in the event of failure.
485
+ * @returns A new {@link AsyncResult} wrapping the continuation result.
486
+ */
487
+ onFailure(cb: FailureContinuation<T>): AsyncResult<T>;
488
+ /**
489
+ * Calls a supplied {@link AsyncFailureContinuation | async failure continuation} if
490
+ * the wrapped result is a failure.
491
+ * @remarks
492
+ * Both synchronous throws and async rejections from the callback are caught
493
+ * and converted to a {@link Failure}.
494
+ * @param cb - The {@link AsyncFailureContinuation | async failure continuation}
495
+ * to be called in the event of failure.
496
+ * @returns A new {@link AsyncResult} wrapping the async continuation result.
497
+ */
498
+ thenOnFailure(cb: AsyncFailureContinuation<T>): AsyncResult<T>;
499
+ /**
500
+ * Calls a supplied {@link ErrorFormatter | error formatter} if
501
+ * the wrapped result is a failure.
502
+ * @param cb - The {@link ErrorFormatter | error formatter} to
503
+ * be called in the event of failure.
504
+ * @returns A new {@link AsyncResult} with the formatted error message,
505
+ * or the original success result.
506
+ */
507
+ withErrorFormat(cb: ErrorFormatter): AsyncResult<T>;
508
+ /**
509
+ * Propagates the wrapped result, appending any error message to the
510
+ * supplied errors aggregator.
511
+ * @param errors - {@link IMessageAggregator | Error aggregator} in which
512
+ * errors will be aggregated.
513
+ * @param formatter - An optional {@link ErrorFormatter | error formatter}
514
+ * to be used to format the error message.
515
+ * @returns A new {@link AsyncResult} wrapping the result after aggregation.
516
+ */
517
+ aggregateError(errors: IMessageAggregator, formatter?: ErrorFormatter): AsyncResult<T>;
518
+ /**
519
+ * Reports the wrapped result to the supplied reporter.
520
+ * @param reporter - The {@link IResultReporter | reporter} to which the result
521
+ * will be reported.
522
+ * @param options - The {@link IResultReportOptions | options} for reporting the result.
523
+ * @returns A new {@link AsyncResult} wrapping the result after reporting.
524
+ */
525
+ report(reporter?: IResultReporter<T>, options?: IResultReportOptions<unknown>): AsyncResult<T>;
526
+ /**
527
+ * Implementation of `PromiseLike.then` enabling `await` on {@link AsyncResult}.
528
+ * @param onfulfilled - Callback invoked when the promise resolves.
529
+ * @param onrejected - Callback invoked when the promise rejects.
530
+ * @returns A `Promise` resolving to the callback result.
531
+ */
532
+ then<TResult1 = Result<T>, TResult2 = never>(onfulfilled?: ((value: Result<T>) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
533
+ /**
534
+ * Creates an {@link AsyncResult} from a {@link Result}.
535
+ * @param result - The {@link Result} to wrap.
536
+ * @returns A new {@link AsyncResult} wrapping the supplied result.
537
+ */
538
+ static from<T>(result: Result<T>): AsyncResult<T>;
539
+ }
540
+
541
+ /**
542
+ * Async continuation callback to be called in the event that a
543
+ * {@link Result} is successful, returning a `Promise` of a new {@link Result}.
544
+ * @public
545
+ */
546
+ export declare type AsyncSuccessContinuation<T, TN> = (value: T) => Promise<Result<TN>>;
547
+
423
548
  declare namespace Base {
424
549
  export {
425
550
  GenericValidatorConstructorParams,
@@ -778,13 +903,23 @@ declare class CacheInvalidatingResultMapWrapper<TK extends string, TSRC, TTARGET
778
903
  toReadOnly(): IReadOnlyResultMap<TK, TSRC>;
779
904
  }
780
905
 
906
+ /**
907
+ * Wraps an async function which might throw to convert exception results
908
+ * to {@link Failure}.
909
+ * @param func - The async function to be captured.
910
+ * @returns Returns a `Promise` of {@link Success} with a value of type `<T>` on
911
+ * success, or {@link Failure} with the thrown error message if `func` throws or rejects.
912
+ * @public
913
+ */
914
+ export declare function captureAsyncResult<T>(func: () => Promise<T>): Promise<Result<T>>;
915
+
781
916
  /**
782
917
  * Wraps a function which might throw to convert exception results
783
918
  * to {@link Failure}.
784
919
  * @param func - The function to be captured.
785
920
  * @returns Returns {@link Success} with a value of type `<T>` on
786
921
  * success , or {@link Failure} with the thrown error message if
787
- * `func` throws an `Error`.
922
+ * `func` throws an `Error` or string.
788
923
  * @public
789
924
  */
790
925
  export declare function captureResult<T>(func: () => T): Result<T>;
@@ -2070,6 +2205,14 @@ declare class Collectible<TKEY extends string = string, TINDEX extends number =
2070
2205
  */
2071
2206
  export declare type ErrorFormatter<TD = unknown> = (message: string, detail?: TD) => string;
2072
2207
 
2208
+ /**
2209
+ * Extracts a message string from an unknown thrown/rejected value.
2210
+ * @param err - The caught error value.
2211
+ * @returns The error message string.
2212
+ * @internal
2213
+ */
2214
+ export declare function _errorMessage(err: unknown): string;
2215
+
2073
2216
  /**
2074
2217
  * Returns {@link Failure | Failure<T>} with the supplied error message.
2075
2218
  * @param message - Error message to be returned.
@@ -2161,6 +2304,14 @@ declare class Collectible<TKEY extends string = string, TINDEX extends number =
2161
2304
  * {@inheritDoc IResult.onFailure}
2162
2305
  */
2163
2306
  onFailure(cb: FailureContinuation<T>): Result<T>;
2307
+ /**
2308
+ * {@inheritDoc IResult.thenOnSuccess}
2309
+ */
2310
+ thenOnSuccess<TN>(__: AsyncSuccessContinuation<T, TN>): AsyncResult<TN>;
2311
+ /**
2312
+ * {@inheritDoc IResult.thenOnFailure}
2313
+ */
2314
+ thenOnFailure(cb: AsyncFailureContinuation<T>): AsyncResult<T>;
2164
2315
  /**
2165
2316
  * {@inheritDoc IResult.withErrorFormat}
2166
2317
  */
@@ -3276,6 +3427,30 @@ declare class Collectible<TKEY extends string = string, TINDEX extends number =
3276
3427
  * was successful, propagates the result value from the successful event.
3277
3428
  */
3278
3429
  onFailure(cb: FailureContinuation<T>): Result<T>;
3430
+ /**
3431
+ * Calls a supplied {@link AsyncSuccessContinuation | async success continuation} if
3432
+ * the operation was a success, bridging into an {@link AsyncResult} chain.
3433
+ * @remarks
3434
+ * If the async callback rejects, the rejection is caught and converted
3435
+ * to a {@link Failure}.
3436
+ * @param cb - The {@link AsyncSuccessContinuation | async success continuation} to
3437
+ * be called in the event of success.
3438
+ * @returns An {@link AsyncResult} wrapping the async continuation result, or
3439
+ * propagating the error message from this failure.
3440
+ */
3441
+ thenOnSuccess<TN>(cb: AsyncSuccessContinuation<T, TN>): AsyncResult<TN>;
3442
+ /**
3443
+ * Calls a supplied {@link AsyncFailureContinuation | async failure continuation} if
3444
+ * the operation failed, bridging into an {@link AsyncResult} chain.
3445
+ * @remarks
3446
+ * If the async callback rejects, the rejection is caught and converted
3447
+ * to a {@link Failure}.
3448
+ * @param cb - The {@link AsyncFailureContinuation | async failure continuation} to
3449
+ * be called in the event of failure.
3450
+ * @returns An {@link AsyncResult} wrapping the async continuation result, or
3451
+ * propagating the success value from this result.
3452
+ */
3453
+ thenOnFailure(cb: AsyncFailureContinuation<T>): AsyncResult<T>;
3279
3454
  /**
3280
3455
  * Calls a supplied {@link ErrorFormatter | error formatter} if
3281
3456
  * the operation failed.
@@ -5557,6 +5732,14 @@ declare class Collectible<TKEY extends string = string, TINDEX extends number =
5557
5732
  * {@inheritDoc IResult.onFailure}
5558
5733
  */
5559
5734
  onFailure(__: FailureContinuation<T>): Result<T>;
5735
+ /**
5736
+ * {@inheritDoc IResult.thenOnSuccess}
5737
+ */
5738
+ thenOnSuccess<TN>(cb: AsyncSuccessContinuation<T, TN>): AsyncResult<TN>;
5739
+ /**
5740
+ * {@inheritDoc IResult.thenOnFailure}
5741
+ */
5742
+ thenOnFailure(__: AsyncFailureContinuation<T>): AsyncResult<T>;
5560
5743
  /**
5561
5744
  * {@inheritDoc IResult.withErrorFormat}
5562
5745
  */
@@ -266,6 +266,30 @@ export interface IResult<T> {
266
266
  * was successful, propagates the result value from the successful event.
267
267
  */
268
268
  onFailure(cb: FailureContinuation<T>): Result<T>;
269
+ /**
270
+ * Calls a supplied {@link AsyncSuccessContinuation | async success continuation} if
271
+ * the operation was a success, bridging into an {@link AsyncResult} chain.
272
+ * @remarks
273
+ * If the async callback rejects, the rejection is caught and converted
274
+ * to a {@link Failure}.
275
+ * @param cb - The {@link AsyncSuccessContinuation | async success continuation} to
276
+ * be called in the event of success.
277
+ * @returns An {@link AsyncResult} wrapping the async continuation result, or
278
+ * propagating the error message from this failure.
279
+ */
280
+ thenOnSuccess<TN>(cb: AsyncSuccessContinuation<T, TN>): AsyncResult<TN>;
281
+ /**
282
+ * Calls a supplied {@link AsyncFailureContinuation | async failure continuation} if
283
+ * the operation failed, bridging into an {@link AsyncResult} chain.
284
+ * @remarks
285
+ * If the async callback rejects, the rejection is caught and converted
286
+ * to a {@link Failure}.
287
+ * @param cb - The {@link AsyncFailureContinuation | async failure continuation} to
288
+ * be called in the event of failure.
289
+ * @returns An {@link AsyncResult} wrapping the async continuation result, or
290
+ * propagating the success value from this result.
291
+ */
292
+ thenOnFailure(cb: AsyncFailureContinuation<T>): AsyncResult<T>;
269
293
  /**
270
294
  * Calls a supplied {@link ErrorFormatter | error formatter} if
271
295
  * the operation failed.
@@ -380,6 +404,14 @@ export declare class Success<out T> implements IResult<T> {
380
404
  * {@inheritDoc IResult.onFailure}
381
405
  */
382
406
  onFailure(__: FailureContinuation<T>): Result<T>;
407
+ /**
408
+ * {@inheritDoc IResult.thenOnSuccess}
409
+ */
410
+ thenOnSuccess<TN>(cb: AsyncSuccessContinuation<T, TN>): AsyncResult<TN>;
411
+ /**
412
+ * {@inheritDoc IResult.thenOnFailure}
413
+ */
414
+ thenOnFailure(__: AsyncFailureContinuation<T>): AsyncResult<T>;
383
415
  /**
384
416
  * {@inheritDoc IResult.withErrorFormat}
385
417
  */
@@ -476,6 +508,14 @@ export declare class Failure<out T> implements IResult<T> {
476
508
  * {@inheritDoc IResult.onFailure}
477
509
  */
478
510
  onFailure(cb: FailureContinuation<T>): Result<T>;
511
+ /**
512
+ * {@inheritDoc IResult.thenOnSuccess}
513
+ */
514
+ thenOnSuccess<TN>(__: AsyncSuccessContinuation<T, TN>): AsyncResult<TN>;
515
+ /**
516
+ * {@inheritDoc IResult.thenOnFailure}
517
+ */
518
+ thenOnFailure(cb: AsyncFailureContinuation<T>): AsyncResult<T>;
479
519
  /**
480
520
  * {@inheritDoc IResult.withErrorFormat}
481
521
  */
@@ -772,14 +812,152 @@ export declare function failsWithDetail<T, TD>(message: string, detail?: TD): De
772
812
  * @public
773
813
  */
774
814
  export declare function propagateWithDetail<T, TD>(result: Result<T>, detail: TD, successDetail?: TD): DetailedResult<T, TD>;
815
+ /**
816
+ * Extracts a message string from an unknown thrown/rejected value.
817
+ * @param err - The caught error value.
818
+ * @returns The error message string.
819
+ * @internal
820
+ */
821
+ export declare function _errorMessage(err: unknown): string;
775
822
  /**
776
823
  * Wraps a function which might throw to convert exception results
777
824
  * to {@link Failure}.
778
825
  * @param func - The function to be captured.
779
826
  * @returns Returns {@link Success} with a value of type `<T>` on
780
827
  * success , or {@link Failure} with the thrown error message if
781
- * `func` throws an `Error`.
828
+ * `func` throws an `Error` or string.
782
829
  * @public
783
830
  */
784
831
  export declare function captureResult<T>(func: () => T): Result<T>;
832
+ /**
833
+ * Async continuation callback to be called in the event that a
834
+ * {@link Result} is successful, returning a `Promise` of a new {@link Result}.
835
+ * @public
836
+ */
837
+ export type AsyncSuccessContinuation<T, TN> = (value: T) => Promise<Result<TN>>;
838
+ /**
839
+ * Async continuation callback to be called in the event that a
840
+ * {@link Result} fails, returning a `Promise` of a new {@link Result}.
841
+ * @public
842
+ */
843
+ export type AsyncFailureContinuation<T> = (message: string) => Promise<Result<T>>;
844
+ /**
845
+ * Wraps a `Promise` of a {@link Result} to enable fluent chaining of both
846
+ * synchronous and asynchronous operations.
847
+ *
848
+ * @remarks
849
+ * `AsyncResult<T>` implements `PromiseLike` so it can be directly `await`ed.
850
+ * Use the `thenOnSuccess` and `thenOnFailure` methods on {@link Result} to bridge
851
+ * from synchronous to asynchronous result chains.
852
+ *
853
+ * @example
854
+ * ```typescript
855
+ * const result: Result<Final> = await parseInput(input)
856
+ * .thenOnSuccess(async (parsed) => fetchData(parsed))
857
+ * .onSuccess((data) => transform(data))
858
+ * .thenOnSuccess(async (transformed) => saveData(transformed))
859
+ * .withErrorFormat((msg) => `pipeline failed: ${msg}`);
860
+ * ```
861
+ *
862
+ * @public
863
+ */
864
+ export declare class AsyncResult<T> implements PromiseLike<Result<T>> {
865
+ private readonly _promise;
866
+ /**
867
+ * Constructs an {@link AsyncResult} wrapping the supplied promise.
868
+ * @remarks
869
+ * If the supplied promise rejects, the rejection is caught and converted
870
+ * to a {@link Failure}, ensuring that awaiting an {@link AsyncResult} always
871
+ * yields a {@link Result}.
872
+ * @param promise - A `Promise` that resolves to a {@link Result}.
873
+ */
874
+ constructor(promise: Promise<Result<T>>);
875
+ /**
876
+ * Calls a supplied {@link SuccessContinuation | success continuation} if
877
+ * the wrapped result is successful.
878
+ * @param cb - The synchronous {@link SuccessContinuation | success continuation}
879
+ * to be called in the event of success.
880
+ * @returns A new {@link AsyncResult} wrapping the continuation result.
881
+ */
882
+ onSuccess<TN>(cb: SuccessContinuation<T, TN>): AsyncResult<TN>;
883
+ /**
884
+ * Calls a supplied {@link AsyncSuccessContinuation | async success continuation} if
885
+ * the wrapped result is successful.
886
+ * @remarks
887
+ * Both synchronous throws and async rejections from the callback are caught
888
+ * and converted to a {@link Failure}.
889
+ * @param cb - The {@link AsyncSuccessContinuation | async success continuation}
890
+ * to be called in the event of success.
891
+ * @returns A new {@link AsyncResult} wrapping the async continuation result.
892
+ */
893
+ thenOnSuccess<TN>(cb: AsyncSuccessContinuation<T, TN>): AsyncResult<TN>;
894
+ /**
895
+ * Calls a supplied {@link FailureContinuation | failure continuation} if
896
+ * the wrapped result is a failure.
897
+ * @param cb - The synchronous {@link FailureContinuation | failure continuation}
898
+ * to be called in the event of failure.
899
+ * @returns A new {@link AsyncResult} wrapping the continuation result.
900
+ */
901
+ onFailure(cb: FailureContinuation<T>): AsyncResult<T>;
902
+ /**
903
+ * Calls a supplied {@link AsyncFailureContinuation | async failure continuation} if
904
+ * the wrapped result is a failure.
905
+ * @remarks
906
+ * Both synchronous throws and async rejections from the callback are caught
907
+ * and converted to a {@link Failure}.
908
+ * @param cb - The {@link AsyncFailureContinuation | async failure continuation}
909
+ * to be called in the event of failure.
910
+ * @returns A new {@link AsyncResult} wrapping the async continuation result.
911
+ */
912
+ thenOnFailure(cb: AsyncFailureContinuation<T>): AsyncResult<T>;
913
+ /**
914
+ * Calls a supplied {@link ErrorFormatter | error formatter} if
915
+ * the wrapped result is a failure.
916
+ * @param cb - The {@link ErrorFormatter | error formatter} to
917
+ * be called in the event of failure.
918
+ * @returns A new {@link AsyncResult} with the formatted error message,
919
+ * or the original success result.
920
+ */
921
+ withErrorFormat(cb: ErrorFormatter): AsyncResult<T>;
922
+ /**
923
+ * Propagates the wrapped result, appending any error message to the
924
+ * supplied errors aggregator.
925
+ * @param errors - {@link IMessageAggregator | Error aggregator} in which
926
+ * errors will be aggregated.
927
+ * @param formatter - An optional {@link ErrorFormatter | error formatter}
928
+ * to be used to format the error message.
929
+ * @returns A new {@link AsyncResult} wrapping the result after aggregation.
930
+ */
931
+ aggregateError(errors: IMessageAggregator, formatter?: ErrorFormatter): AsyncResult<T>;
932
+ /**
933
+ * Reports the wrapped result to the supplied reporter.
934
+ * @param reporter - The {@link IResultReporter | reporter} to which the result
935
+ * will be reported.
936
+ * @param options - The {@link IResultReportOptions | options} for reporting the result.
937
+ * @returns A new {@link AsyncResult} wrapping the result after reporting.
938
+ */
939
+ report(reporter?: IResultReporter<T>, options?: IResultReportOptions<unknown>): AsyncResult<T>;
940
+ /**
941
+ * Implementation of `PromiseLike.then` enabling `await` on {@link AsyncResult}.
942
+ * @param onfulfilled - Callback invoked when the promise resolves.
943
+ * @param onrejected - Callback invoked when the promise rejects.
944
+ * @returns A `Promise` resolving to the callback result.
945
+ */
946
+ then<TResult1 = Result<T>, TResult2 = never>(onfulfilled?: ((value: Result<T>) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
947
+ /**
948
+ * Creates an {@link AsyncResult} from a {@link Result}.
949
+ * @param result - The {@link Result} to wrap.
950
+ * @returns A new {@link AsyncResult} wrapping the supplied result.
951
+ */
952
+ static from<T>(result: Result<T>): AsyncResult<T>;
953
+ }
954
+ /**
955
+ * Wraps an async function which might throw to convert exception results
956
+ * to {@link Failure}.
957
+ * @param func - The async function to be captured.
958
+ * @returns Returns a `Promise` of {@link Success} with a value of type `<T>` on
959
+ * success, or {@link Failure} with the thrown error message if `func` throws or rejects.
960
+ * @public
961
+ */
962
+ export declare function captureAsyncResult<T>(func: () => Promise<T>): Promise<Result<T>>;
785
963
  //# sourceMappingURL=result.d.ts.map
@@ -21,7 +21,7 @@
21
21
  * SOFTWARE.
22
22
  */
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.DetailedFailure = exports.DetailedSuccess = exports.Failure = exports.Success = void 0;
24
+ exports.AsyncResult = exports.DetailedFailure = exports.DetailedSuccess = exports.Failure = exports.Success = void 0;
25
25
  exports.isDeferredResult = isDeferredResult;
26
26
  exports.succeed = succeed;
27
27
  exports.succeeds = succeeds;
@@ -33,7 +33,9 @@ exports.succeedsWithDetail = succeedsWithDetail;
33
33
  exports.failWithDetail = failWithDetail;
34
34
  exports.failsWithDetail = failsWithDetail;
35
35
  exports.propagateWithDetail = propagateWithDetail;
36
+ exports._errorMessage = _errorMessage;
36
37
  exports.captureResult = captureResult;
38
+ exports.captureAsyncResult = captureAsyncResult;
37
39
  /**
38
40
  * Checks if a result is a deferred result.
39
41
  * @param result - The result to check.
@@ -116,6 +118,26 @@ class Success {
116
118
  onFailure(__) {
117
119
  return this;
118
120
  }
121
+ /**
122
+ * {@inheritDoc IResult.thenOnSuccess}
123
+ */
124
+ thenOnSuccess(cb) {
125
+ try {
126
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
127
+ return new AsyncResult(cb(this._value));
128
+ }
129
+ catch (err) {
130
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
131
+ return AsyncResult.from(fail(_errorMessage(err)));
132
+ }
133
+ }
134
+ /**
135
+ * {@inheritDoc IResult.thenOnFailure}
136
+ */
137
+ thenOnFailure(__) {
138
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
139
+ return AsyncResult.from(this);
140
+ }
119
141
  /**
120
142
  * {@inheritDoc IResult.withErrorFormat}
121
143
  */
@@ -242,6 +264,26 @@ class Failure {
242
264
  onFailure(cb) {
243
265
  return cb(this._message);
244
266
  }
267
+ /**
268
+ * {@inheritDoc IResult.thenOnSuccess}
269
+ */
270
+ thenOnSuccess(__) {
271
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
272
+ return AsyncResult.from(fail(this._message));
273
+ }
274
+ /**
275
+ * {@inheritDoc IResult.thenOnFailure}
276
+ */
277
+ thenOnFailure(cb) {
278
+ try {
279
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
280
+ return new AsyncResult(cb(this._message));
281
+ }
282
+ catch (err) {
283
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
284
+ return AsyncResult.from(fail(_errorMessage(err)));
285
+ }
286
+ }
245
287
  /**
246
288
  * {@inheritDoc IResult.withErrorFormat}
247
289
  */
@@ -610,13 +652,25 @@ function propagateWithDetail(result, detail, successDetail) {
610
652
  ? succeedWithDetail(result.value, successDetail !== null && successDetail !== void 0 ? successDetail : detail)
611
653
  : failWithDetail(result.message, detail);
612
654
  }
655
+ /**
656
+ * Extracts a message string from an unknown thrown/rejected value.
657
+ * @param err - The caught error value.
658
+ * @returns The error message string.
659
+ * @internal
660
+ */
661
+ function _errorMessage(err) {
662
+ if (err instanceof Error) {
663
+ return err.message;
664
+ }
665
+ return String(err);
666
+ }
613
667
  /**
614
668
  * Wraps a function which might throw to convert exception results
615
669
  * to {@link Failure}.
616
670
  * @param func - The function to be captured.
617
671
  * @returns Returns {@link Success} with a value of type `<T>` on
618
672
  * success , or {@link Failure} with the thrown error message if
619
- * `func` throws an `Error`.
673
+ * `func` throws an `Error` or string.
620
674
  * @public
621
675
  */
622
676
  function captureResult(func) {
@@ -624,7 +678,181 @@ function captureResult(func) {
624
678
  return succeed(func());
625
679
  }
626
680
  catch (err) {
627
- return fail(err.message);
681
+ return fail(_errorMessage(err));
682
+ }
683
+ }
684
+ /**
685
+ * Wraps a `Promise` of a {@link Result} to enable fluent chaining of both
686
+ * synchronous and asynchronous operations.
687
+ *
688
+ * @remarks
689
+ * `AsyncResult<T>` implements `PromiseLike` so it can be directly `await`ed.
690
+ * Use the `thenOnSuccess` and `thenOnFailure` methods on {@link Result} to bridge
691
+ * from synchronous to asynchronous result chains.
692
+ *
693
+ * @example
694
+ * ```typescript
695
+ * const result: Result<Final> = await parseInput(input)
696
+ * .thenOnSuccess(async (parsed) => fetchData(parsed))
697
+ * .onSuccess((data) => transform(data))
698
+ * .thenOnSuccess(async (transformed) => saveData(transformed))
699
+ * .withErrorFormat((msg) => `pipeline failed: ${msg}`);
700
+ * ```
701
+ *
702
+ * @public
703
+ */
704
+ class AsyncResult {
705
+ /**
706
+ * Constructs an {@link AsyncResult} wrapping the supplied promise.
707
+ * @remarks
708
+ * If the supplied promise rejects, the rejection is caught and converted
709
+ * to a {@link Failure}, ensuring that awaiting an {@link AsyncResult} always
710
+ * yields a {@link Result}.
711
+ * @param promise - A `Promise` that resolves to a {@link Result}.
712
+ */
713
+ constructor(promise) {
714
+ this._promise = promise.catch((err) => fail(_errorMessage(err)));
715
+ }
716
+ /**
717
+ * Calls a supplied {@link SuccessContinuation | success continuation} if
718
+ * the wrapped result is successful.
719
+ * @param cb - The synchronous {@link SuccessContinuation | success continuation}
720
+ * to be called in the event of success.
721
+ * @returns A new {@link AsyncResult} wrapping the continuation result.
722
+ */
723
+ onSuccess(cb) {
724
+ return new AsyncResult(this._promise.then((r) => r.onSuccess(cb)));
725
+ }
726
+ /**
727
+ * Calls a supplied {@link AsyncSuccessContinuation | async success continuation} if
728
+ * the wrapped result is successful.
729
+ * @remarks
730
+ * Both synchronous throws and async rejections from the callback are caught
731
+ * and converted to a {@link Failure}.
732
+ * @param cb - The {@link AsyncSuccessContinuation | async success continuation}
733
+ * to be called in the event of success.
734
+ * @returns A new {@link AsyncResult} wrapping the async continuation result.
735
+ */
736
+ thenOnSuccess(cb) {
737
+ return new AsyncResult(this._promise.then(async (r) => {
738
+ if (r.isFailure()) {
739
+ return fail(r.message);
740
+ }
741
+ try {
742
+ return await cb(r.value);
743
+ }
744
+ catch (err) {
745
+ return fail(_errorMessage(err));
746
+ }
747
+ }));
748
+ }
749
+ /**
750
+ * Calls a supplied {@link FailureContinuation | failure continuation} if
751
+ * the wrapped result is a failure.
752
+ * @param cb - The synchronous {@link FailureContinuation | failure continuation}
753
+ * to be called in the event of failure.
754
+ * @returns A new {@link AsyncResult} wrapping the continuation result.
755
+ */
756
+ onFailure(cb) {
757
+ return new AsyncResult(this._promise.then((r) => r.onFailure(cb)));
758
+ }
759
+ /**
760
+ * Calls a supplied {@link AsyncFailureContinuation | async failure continuation} if
761
+ * the wrapped result is a failure.
762
+ * @remarks
763
+ * Both synchronous throws and async rejections from the callback are caught
764
+ * and converted to a {@link Failure}.
765
+ * @param cb - The {@link AsyncFailureContinuation | async failure continuation}
766
+ * to be called in the event of failure.
767
+ * @returns A new {@link AsyncResult} wrapping the async continuation result.
768
+ */
769
+ thenOnFailure(cb) {
770
+ return new AsyncResult(this._promise.then(async (r) => {
771
+ if (r.isSuccess()) {
772
+ return r;
773
+ }
774
+ try {
775
+ return await cb(r.message);
776
+ }
777
+ catch (err) {
778
+ return fail(_errorMessage(err));
779
+ }
780
+ }));
781
+ }
782
+ /**
783
+ * Calls a supplied {@link ErrorFormatter | error formatter} if
784
+ * the wrapped result is a failure.
785
+ * @param cb - The {@link ErrorFormatter | error formatter} to
786
+ * be called in the event of failure.
787
+ * @returns A new {@link AsyncResult} with the formatted error message,
788
+ * or the original success result.
789
+ */
790
+ withErrorFormat(cb) {
791
+ return new AsyncResult(this._promise.then((r) => r.withErrorFormat(cb)));
792
+ }
793
+ /**
794
+ * Propagates the wrapped result, appending any error message to the
795
+ * supplied errors aggregator.
796
+ * @param errors - {@link IMessageAggregator | Error aggregator} in which
797
+ * errors will be aggregated.
798
+ * @param formatter - An optional {@link ErrorFormatter | error formatter}
799
+ * to be used to format the error message.
800
+ * @returns A new {@link AsyncResult} wrapping the result after aggregation.
801
+ */
802
+ aggregateError(errors, formatter) {
803
+ return new AsyncResult(this._promise.then((r) => {
804
+ r.aggregateError(errors, formatter);
805
+ return r;
806
+ }));
807
+ }
808
+ /**
809
+ * Reports the wrapped result to the supplied reporter.
810
+ * @param reporter - The {@link IResultReporter | reporter} to which the result
811
+ * will be reported.
812
+ * @param options - The {@link IResultReportOptions | options} for reporting the result.
813
+ * @returns A new {@link AsyncResult} wrapping the result after reporting.
814
+ */
815
+ report(reporter, options) {
816
+ return new AsyncResult(this._promise.then((r) => {
817
+ r.report(reporter, options);
818
+ return r;
819
+ }));
820
+ }
821
+ /**
822
+ * Implementation of `PromiseLike.then` enabling `await` on {@link AsyncResult}.
823
+ * @param onfulfilled - Callback invoked when the promise resolves.
824
+ * @param onrejected - Callback invoked when the promise rejects.
825
+ * @returns A `Promise` resolving to the callback result.
826
+ */
827
+ /* eslint-disable @rushstack/no-new-null */
828
+ then(onfulfilled, onrejected) {
829
+ /* eslint-enable @rushstack/no-new-null */
830
+ return this._promise.then(onfulfilled, onrejected);
831
+ }
832
+ /**
833
+ * Creates an {@link AsyncResult} from a {@link Result}.
834
+ * @param result - The {@link Result} to wrap.
835
+ * @returns A new {@link AsyncResult} wrapping the supplied result.
836
+ */
837
+ static from(result) {
838
+ return new AsyncResult(Promise.resolve(result));
839
+ }
840
+ }
841
+ exports.AsyncResult = AsyncResult;
842
+ /**
843
+ * Wraps an async function which might throw to convert exception results
844
+ * to {@link Failure}.
845
+ * @param func - The async function to be captured.
846
+ * @returns Returns a `Promise` of {@link Success} with a value of type `<T>` on
847
+ * success, or {@link Failure} with the thrown error message if `func` throws or rejects.
848
+ * @public
849
+ */
850
+ async function captureAsyncResult(func) {
851
+ try {
852
+ return succeed(await func());
853
+ }
854
+ catch (err) {
855
+ return fail(_errorMessage(err));
628
856
  }
629
857
  }
630
858
  //# sourceMappingURL=result.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fgv/ts-utils",
3
- "version": "5.1.0-11",
3
+ "version": "5.1.0-12",
4
4
  "description": "Assorted Typescript Utilities",
5
5
  "main": "lib/index.js",
6
6
  "module": "dist/index.js",
@@ -54,8 +54,8 @@
54
54
  "@rushstack/heft-jest-plugin": "1.2.6",
55
55
  "eslint-plugin-tsdoc": "~0.5.2",
56
56
  "typedoc": "~0.28.16",
57
- "@fgv/heft-dual-rig": "5.1.0-11",
58
- "@fgv/typedoc-compact-theme": "5.1.0-11"
57
+ "@fgv/heft-dual-rig": "5.1.0-12",
58
+ "@fgv/typedoc-compact-theme": "5.1.0-12"
59
59
  },
60
60
  "repository": {
61
61
  "type": "git",