unthrown 3.0.1 → 4.0.0
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 +5 -2
- package/dist/index.cjs +215 -43
- package/dist/index.d.cts +556 -120
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +556 -120
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +215 -43
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -5
- package/docs/index.md +0 -1407
package/README.md
CHANGED
|
@@ -11,7 +11,10 @@ pnpm add unthrown
|
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
|
-
import {
|
|
14
|
+
import { fromPromise, TaggedError } from "unthrown";
|
|
15
|
+
|
|
16
|
+
class NotFound extends TaggedError("NotFound") {} // our modeled domain failure
|
|
17
|
+
class NotFoundError extends Error {} // what `fetchUser` rejects with on a 404
|
|
15
18
|
|
|
16
19
|
const user = fromPromise(fetchUser(id), (cause, defect) =>
|
|
17
20
|
cause instanceof NotFoundError ? new NotFound() : defect(cause),
|
|
@@ -37,4 +40,4 @@ and complete API.
|
|
|
37
40
|
|
|
38
41
|
## License
|
|
39
42
|
|
|
40
|
-
[MIT](
|
|
43
|
+
[MIT](https://github.com/btravstack/unthrown/blob/main/LICENSE) © Benoit TRAVERS
|
package/dist/index.cjs
CHANGED
|
@@ -14,7 +14,14 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
14
14
|
* A `Defect` is never wrapped in an `UnwrapError`: its original cause is
|
|
15
15
|
* re-thrown (with its original stack) instead.
|
|
16
16
|
*
|
|
17
|
+
* `unwrap()` and `unwrapErr()` are type-gated (`this: Result<T, never>` /
|
|
18
|
+
* `Result<never, E>`), so the wrong-variant branch that throws this is
|
|
19
|
+
* unreachable through well-typed code — it remains only as a defensive guard
|
|
20
|
+
* against unsound runtime misuse (e.g. an `as` cast past the gate).
|
|
21
|
+
*
|
|
17
22
|
* @typeParam E - the type of the {@link UnwrapError.error} it carries.
|
|
23
|
+
*
|
|
24
|
+
* @category Errors
|
|
18
25
|
*/
|
|
19
26
|
var UnwrapError = class extends Error {
|
|
20
27
|
/**
|
|
@@ -129,7 +136,7 @@ var Res = class {
|
|
|
129
136
|
f(this.error);
|
|
130
137
|
return this;
|
|
131
138
|
} catch (cause) {
|
|
132
|
-
return
|
|
139
|
+
return observerThrowToDefect(cause, this.error);
|
|
133
140
|
}
|
|
134
141
|
}
|
|
135
142
|
flatTapErr(f) {
|
|
@@ -138,7 +145,7 @@ var Res = class {
|
|
|
138
145
|
const r = f(this.error);
|
|
139
146
|
return r.tag === "Ok" ? this : passThrough(r);
|
|
140
147
|
} catch (cause) {
|
|
141
|
-
return
|
|
148
|
+
return observerThrowToDefect(cause, this.error);
|
|
142
149
|
}
|
|
143
150
|
}
|
|
144
151
|
recoverDefect(f) {
|
|
@@ -155,7 +162,7 @@ var Res = class {
|
|
|
155
162
|
f(this.cause);
|
|
156
163
|
return this;
|
|
157
164
|
} catch (cause) {
|
|
158
|
-
return
|
|
165
|
+
return observerThrowToDefect(cause, this.cause);
|
|
159
166
|
}
|
|
160
167
|
}
|
|
161
168
|
match(cases) {
|
|
@@ -218,10 +225,10 @@ const RESULT_PROTO = Res.prototype;
|
|
|
218
225
|
* @internal
|
|
219
226
|
*/
|
|
220
227
|
function okRes(value) {
|
|
221
|
-
return Object.assign(Object.create(RESULT_PROTO), {
|
|
228
|
+
return Object.freeze(Object.assign(Object.create(RESULT_PROTO), {
|
|
222
229
|
tag: "Ok",
|
|
223
230
|
value
|
|
224
|
-
});
|
|
231
|
+
}));
|
|
225
232
|
}
|
|
226
233
|
/**
|
|
227
234
|
* Construct an `Err` result.
|
|
@@ -229,10 +236,10 @@ function okRes(value) {
|
|
|
229
236
|
* @internal
|
|
230
237
|
*/
|
|
231
238
|
function errRes(error) {
|
|
232
|
-
return Object.assign(Object.create(RESULT_PROTO), {
|
|
239
|
+
return Object.freeze(Object.assign(Object.create(RESULT_PROTO), {
|
|
233
240
|
tag: "Err",
|
|
234
241
|
error
|
|
235
|
-
});
|
|
242
|
+
}));
|
|
236
243
|
}
|
|
237
244
|
/**
|
|
238
245
|
* Construct a `Defect` result.
|
|
@@ -240,10 +247,10 @@ function errRes(error) {
|
|
|
240
247
|
* @internal
|
|
241
248
|
*/
|
|
242
249
|
function defectRes(cause) {
|
|
243
|
-
return Object.assign(Object.create(RESULT_PROTO), {
|
|
250
|
+
return Object.freeze(Object.assign(Object.create(RESULT_PROTO), {
|
|
244
251
|
tag: "Defect",
|
|
245
252
|
cause
|
|
246
|
-
});
|
|
253
|
+
}));
|
|
247
254
|
}
|
|
248
255
|
/**
|
|
249
256
|
* Type guard: is `x` a {@link Result} (any of `Ok` / `Err` / `Defect`)?
|
|
@@ -256,6 +263,20 @@ function defectRes(cause) {
|
|
|
256
263
|
* is not a `Result` and returns `false`.
|
|
257
264
|
*
|
|
258
265
|
* @returns `true` when `x` is a `Result` produced by this library.
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```ts
|
|
269
|
+
* import { isResult, Ok } from "unthrown";
|
|
270
|
+
*
|
|
271
|
+
* isResult(Ok(1)); // => true
|
|
272
|
+
* isResult({ tag: "Ok" }); // => false (look-alike, wrong prototype)
|
|
273
|
+
* isResult(Ok(1).toAsync()); // => false (an AsyncResult is not a Result)
|
|
274
|
+
*
|
|
275
|
+
* const x: unknown = Ok(1);
|
|
276
|
+
* if (isResult(x)) x.match({ ok: () => 1, err: () => 0, defect: () => -1 });
|
|
277
|
+
* ```
|
|
278
|
+
*
|
|
279
|
+
* @category Guards
|
|
259
280
|
*/
|
|
260
281
|
function isResult(x) {
|
|
261
282
|
return x instanceof Res;
|
|
@@ -274,6 +295,18 @@ function passThrough(self) {
|
|
|
274
295
|
return self;
|
|
275
296
|
}
|
|
276
297
|
/**
|
|
298
|
+
* A throw inside a *failure observer* (`tapErr` / `tapDefect` / `flatTapErr`)
|
|
299
|
+
* must not destroy the failure being observed — that is the exact place (e.g. a
|
|
300
|
+
* failing error-logger) where losing the underlying failure hurts most. The
|
|
301
|
+
* resulting Defect aggregates both: `errors[0]` is the observer's throw,
|
|
302
|
+
* `errors[1]` the original failure.
|
|
303
|
+
*
|
|
304
|
+
* @internal
|
|
305
|
+
*/
|
|
306
|
+
function observerThrowToDefect(thrown, original) {
|
|
307
|
+
return defectRes(new AggregateError([thrown, original], "unthrown: a failure-observer callback threw; errors[0] is the callback's throw, errors[1] the original failure"));
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
277
310
|
* Validate that a `bind`/`let` scope is a real (non-null) object before merging a
|
|
278
311
|
* key into it.
|
|
279
312
|
*
|
|
@@ -292,7 +325,7 @@ function passThrough(self) {
|
|
|
292
325
|
* @internal
|
|
293
326
|
*/
|
|
294
327
|
function scopeOf(value) {
|
|
295
|
-
if (typeof value !== "object" || value === null) throw new TypeError("bind/let requires an object scope — start a do-chain with Do()");
|
|
328
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("bind/let requires an object scope — start a do-chain with Do()");
|
|
296
329
|
return value;
|
|
297
330
|
}
|
|
298
331
|
/**
|
|
@@ -420,7 +453,7 @@ var AsyncRes = class AsyncRes {
|
|
|
420
453
|
f(r.error);
|
|
421
454
|
return r;
|
|
422
455
|
} catch (cause) {
|
|
423
|
-
return
|
|
456
|
+
return observerThrowToDefect(cause, r.error);
|
|
424
457
|
}
|
|
425
458
|
}));
|
|
426
459
|
}
|
|
@@ -431,7 +464,7 @@ var AsyncRes = class AsyncRes {
|
|
|
431
464
|
const inner = await f(r.error);
|
|
432
465
|
return inner.tag === "Ok" ? passThrough(r) : passThrough(inner);
|
|
433
466
|
} catch (cause) {
|
|
434
|
-
return
|
|
467
|
+
return observerThrowToDefect(cause, r.error);
|
|
435
468
|
}
|
|
436
469
|
}));
|
|
437
470
|
}
|
|
@@ -452,7 +485,7 @@ var AsyncRes = class AsyncRes {
|
|
|
452
485
|
f(r.cause);
|
|
453
486
|
return r;
|
|
454
487
|
} catch (cause) {
|
|
455
|
-
return
|
|
488
|
+
return observerThrowToDefect(cause, r.cause);
|
|
456
489
|
}
|
|
457
490
|
}));
|
|
458
491
|
}
|
|
@@ -489,8 +522,12 @@ var AsyncRes = class AsyncRes {
|
|
|
489
522
|
* @example
|
|
490
523
|
* ```ts
|
|
491
524
|
* import { Ok } from "unthrown";
|
|
492
|
-
*
|
|
525
|
+
*
|
|
526
|
+
* Ok(2).map((n) => n + 1); // => Ok(3)
|
|
527
|
+
* Ok(42).unwrap(); // => 42
|
|
493
528
|
* ```
|
|
529
|
+
*
|
|
530
|
+
* @category Constructors
|
|
494
531
|
*/
|
|
495
532
|
function Ok(value) {
|
|
496
533
|
return okRes(value);
|
|
@@ -504,8 +541,12 @@ function Ok(value) {
|
|
|
504
541
|
* @example
|
|
505
542
|
* ```ts
|
|
506
543
|
* import { Err } from "unthrown";
|
|
507
|
-
*
|
|
544
|
+
*
|
|
545
|
+
* Err("not_found").map((n) => n + 1); // => Err("not_found") (map skipped)
|
|
546
|
+
* Err("not_found").unwrapErr(); // => "not_found"
|
|
508
547
|
* ```
|
|
548
|
+
*
|
|
549
|
+
* @category Constructors
|
|
509
550
|
*/
|
|
510
551
|
function Err(error) {
|
|
511
552
|
return errRes(error);
|
|
@@ -517,10 +558,16 @@ function Err(error) {
|
|
|
517
558
|
*
|
|
518
559
|
* @example
|
|
519
560
|
* ```ts
|
|
520
|
-
* import { isOk, type Result } from "unthrown";
|
|
561
|
+
* import { isOk, Ok, Err, type Result } from "unthrown";
|
|
562
|
+
*
|
|
563
|
+
* isOk(Ok(1)); // => true
|
|
564
|
+
* isOk(Err("boom")); // => false
|
|
565
|
+
*
|
|
521
566
|
* declare const r: Result<number, string>;
|
|
522
567
|
* if (isOk(r)) r.value; // number, narrowed
|
|
523
568
|
* ```
|
|
569
|
+
*
|
|
570
|
+
* @category Guards
|
|
524
571
|
*/
|
|
525
572
|
function isOk(r) {
|
|
526
573
|
return r.tag === "Ok";
|
|
@@ -529,6 +576,19 @@ function isOk(r) {
|
|
|
529
576
|
* Type guard: narrow a {@link Result} to its `Err` variant, exposing `.error`.
|
|
530
577
|
*
|
|
531
578
|
* @returns `true` when `r` is `Err`.
|
|
579
|
+
*
|
|
580
|
+
* @example
|
|
581
|
+
* ```ts
|
|
582
|
+
* import { isErr, Ok, Err, type Result } from "unthrown";
|
|
583
|
+
*
|
|
584
|
+
* isErr(Err("boom")); // => true
|
|
585
|
+
* isErr(Ok(1)); // => false
|
|
586
|
+
*
|
|
587
|
+
* declare const r: Result<number, string>;
|
|
588
|
+
* if (isErr(r)) r.error; // string, narrowed
|
|
589
|
+
* ```
|
|
590
|
+
*
|
|
591
|
+
* @category Guards
|
|
532
592
|
*/
|
|
533
593
|
function isErr(r) {
|
|
534
594
|
return r.tag === "Err";
|
|
@@ -536,7 +596,27 @@ function isErr(r) {
|
|
|
536
596
|
/**
|
|
537
597
|
* Type guard: narrow a {@link Result} to its `Defect` variant, exposing `.cause`.
|
|
538
598
|
*
|
|
599
|
+
* @remarks
|
|
600
|
+
* A `Defect` has no public constructor — it only arises at a boundary (e.g. a
|
|
601
|
+
* callback throwing inside a combinator). This guard is how you detect one.
|
|
602
|
+
*
|
|
539
603
|
* @returns `true` when `r` is a `Defect`.
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* ```ts
|
|
607
|
+
* import { isDefect, Ok } from "unthrown";
|
|
608
|
+
*
|
|
609
|
+
* // A throw inside a combinator is captured as a Defect:
|
|
610
|
+
* const r = Ok(1).map(() => {
|
|
611
|
+
* throw new Error("boom");
|
|
612
|
+
* });
|
|
613
|
+
* isDefect(r); // => true
|
|
614
|
+
* isDefect(Ok(1)); // => false
|
|
615
|
+
*
|
|
616
|
+
* if (isDefect(r)) r.cause; // unknown, narrowed
|
|
617
|
+
* ```
|
|
618
|
+
*
|
|
619
|
+
* @category Guards
|
|
540
620
|
*/
|
|
541
621
|
function isDefect(r) {
|
|
542
622
|
return r.tag === "Defect";
|
|
@@ -564,6 +644,24 @@ function isDefect(r) {
|
|
|
564
644
|
* .map(({ user, org, label }) => render(user, org, label));
|
|
565
645
|
* // Result<View, NotFound>
|
|
566
646
|
* ```
|
|
647
|
+
*
|
|
648
|
+
* @example
|
|
649
|
+
* ```ts
|
|
650
|
+
* import { Do, Ok, Err } from "unthrown";
|
|
651
|
+
*
|
|
652
|
+
* // Ok path — the scope accumulates:
|
|
653
|
+
* Do()
|
|
654
|
+
* .bind("a", () => Ok(2))
|
|
655
|
+
* .let("b", ({ a }) => a * 10)
|
|
656
|
+
* .map(({ a, b }) => a + b); // => Ok(22)
|
|
657
|
+
*
|
|
658
|
+
* // Err path — the first Err short-circuits the rest:
|
|
659
|
+
* Do()
|
|
660
|
+
* .bind("a", () => Err("boom"))
|
|
661
|
+
* .let("b", ({ a }) => a); // => Err("boom")
|
|
662
|
+
* ```
|
|
663
|
+
*
|
|
664
|
+
* @category Do-notation
|
|
567
665
|
*/
|
|
568
666
|
function Do() {
|
|
569
667
|
return Ok({});
|
|
@@ -613,10 +711,16 @@ function isDefectMarker(x) {
|
|
|
613
711
|
* @param value - the possibly-absent value.
|
|
614
712
|
* @param onAbsent - lazily produces the error for the absent case.
|
|
615
713
|
*
|
|
714
|
+
* @category Interop
|
|
715
|
+
*
|
|
616
716
|
* @example
|
|
617
717
|
* ```ts
|
|
618
718
|
* import { fromNullable } from "unthrown";
|
|
619
|
-
*
|
|
719
|
+
*
|
|
720
|
+
* const map = new Map([["a", 1]]);
|
|
721
|
+
* fromNullable(map.get("a"), () => "absent").unwrapOr(0); // => 1
|
|
722
|
+
* fromNullable(map.get("z"), () => "absent"); // => Err("absent")
|
|
723
|
+
* fromNullable(0, () => "absent").unwrapOr(-1); // => 0 (falsy but present)
|
|
620
724
|
* ```
|
|
621
725
|
*/
|
|
622
726
|
function fromNullable(value, onAbsent) {
|
|
@@ -647,11 +751,21 @@ function fromNullable(value, onAbsent) {
|
|
|
647
751
|
* unmodeled by returning `defect(cause)` (the helper passed as its second arg).
|
|
648
752
|
* @returns a function with the same arguments returning `Result<T, E>`.
|
|
649
753
|
*
|
|
754
|
+
* @category Interop
|
|
755
|
+
*
|
|
650
756
|
* @example
|
|
651
757
|
* ```ts
|
|
652
758
|
* import { fromThrowable } from "unthrown";
|
|
653
|
-
*
|
|
654
|
-
* parse
|
|
759
|
+
*
|
|
760
|
+
* // Model the parse failure as an `Err`, everything unexpected as a `Defect`.
|
|
761
|
+
* const parse = fromThrowable(
|
|
762
|
+
* (text: string) => JSON.parse(text) as unknown,
|
|
763
|
+
* (cause, defect) =>
|
|
764
|
+
* cause instanceof SyntaxError ? ("invalid_json" as const) : defect(cause),
|
|
765
|
+
* );
|
|
766
|
+
*
|
|
767
|
+
* parse('{"ok":true}').unwrapOr(null); // => { ok: true }
|
|
768
|
+
* parse("nope"); // => Err("invalid_json")
|
|
655
769
|
* ```
|
|
656
770
|
*/
|
|
657
771
|
function fromThrowable(fn, qualify) {
|
|
@@ -686,17 +800,24 @@ function fromThrowable(fn, qualify) {
|
|
|
686
800
|
* @param qualify - triages a rejection `cause` into a modeled `E`, or marks it
|
|
687
801
|
* unmodeled by returning `defect(cause)` (the helper passed as its second arg).
|
|
688
802
|
*
|
|
803
|
+
* @category Interop
|
|
804
|
+
*
|
|
689
805
|
* @example
|
|
690
806
|
* ```ts
|
|
691
807
|
* import { fromPromise } from "unthrown";
|
|
808
|
+
*
|
|
809
|
+
* // A rejection with a NotFoundError becomes a modeled `Err`; anything else a Defect.
|
|
692
810
|
* const user = await fromPromise(fetchUser(id), (cause, defect) =>
|
|
693
811
|
* cause instanceof NotFoundError ? ("not_found" as const) : defect(cause),
|
|
694
812
|
* );
|
|
813
|
+
*
|
|
814
|
+
* if (user.isOk()) user.value; // => the fetched user
|
|
815
|
+
* // when fetchUser rejects with NotFoundError: user is Err("not_found")
|
|
695
816
|
* ```
|
|
696
817
|
*/
|
|
697
818
|
function fromPromise(promise, qualify) {
|
|
698
819
|
const triage = qualify;
|
|
699
|
-
return new AsyncRes((typeof promise === "function" ? Promise.resolve().then(promise) : promise).then((value) => okRes(value), (cause) => qualifyToResult(cause, triage)));
|
|
820
|
+
return new AsyncRes((typeof promise === "function" ? Promise.resolve().then(promise) : Promise.resolve(promise)).then((value) => okRes(value), (cause) => qualifyToResult(cause, triage)));
|
|
700
821
|
}
|
|
701
822
|
/**
|
|
702
823
|
* Wrap a `Promise` asserted **not** to fail in any modeled way: any rejection
|
|
@@ -709,9 +830,20 @@ function fromPromise(promise, qualify) {
|
|
|
709
830
|
*
|
|
710
831
|
* @typeParam T - the resolved value type.
|
|
711
832
|
* @param promise - the promise, or a thunk returning one.
|
|
833
|
+
*
|
|
834
|
+
* @category Interop
|
|
835
|
+
*
|
|
836
|
+
* @example
|
|
837
|
+
* ```ts
|
|
838
|
+
* import { fromSafePromise } from "unthrown";
|
|
839
|
+
*
|
|
840
|
+
* (await fromSafePromise(Promise.resolve(3))).unwrap(); // => 3
|
|
841
|
+
* // a rejection becomes a Defect (never a modeled Err):
|
|
842
|
+
* await fromSafePromise(Promise.reject(new Error("boom"))); // => Defect(Error("boom"))
|
|
843
|
+
* ```
|
|
712
844
|
*/
|
|
713
845
|
function fromSafePromise(promise) {
|
|
714
|
-
return new AsyncRes((typeof promise === "function" ? Promise.resolve().then(promise) : promise).then((value) => okRes(value), (cause) => defectRes(cause)));
|
|
846
|
+
return new AsyncRes((typeof promise === "function" ? Promise.resolve().then(promise) : Promise.resolve(promise)).then((value) => okRes(value), (cause) => defectRes(cause)));
|
|
715
847
|
}
|
|
716
848
|
function qualifyToResult(cause, qualify) {
|
|
717
849
|
try {
|
|
@@ -749,8 +881,10 @@ function foldRecord(results) {
|
|
|
749
881
|
let firstErr;
|
|
750
882
|
let firstDefect;
|
|
751
883
|
const values = {};
|
|
752
|
-
for (const [key, r] of Object.entries(results)) if (r.tag === "Defect")
|
|
753
|
-
|
|
884
|
+
for (const [key, r] of Object.entries(results)) if (r.tag === "Defect") {
|
|
885
|
+
firstDefect ??= r;
|
|
886
|
+
break;
|
|
887
|
+
} else if (r.tag === "Err") firstErr ??= r;
|
|
754
888
|
else Object.defineProperty(values, key, {
|
|
755
889
|
value: r.value,
|
|
756
890
|
enumerable: true,
|
|
@@ -771,11 +905,14 @@ function foldRecord(results) {
|
|
|
771
905
|
* collapses to `Result<T[], E>` with no cast. For a **record** keyed by name,
|
|
772
906
|
* use {@link allFromDict}.
|
|
773
907
|
*
|
|
908
|
+
* @category Aggregate
|
|
909
|
+
*
|
|
774
910
|
* @example
|
|
775
911
|
* ```ts
|
|
776
|
-
* import { all, Ok } from "unthrown";
|
|
777
|
-
*
|
|
778
|
-
* all([Ok(1), Ok(
|
|
912
|
+
* import { all, Ok, Err } from "unthrown";
|
|
913
|
+
*
|
|
914
|
+
* all([Ok(1), Ok("a"), Ok(true)]).unwrap(); // => [1, "a", true] (typed [number, string, boolean])
|
|
915
|
+
* all([Ok(1), Err("e"), Ok(3)]); // => Err("e") (short-circuits on the first Err)
|
|
779
916
|
* ```
|
|
780
917
|
*/
|
|
781
918
|
function all(results) {
|
|
@@ -791,10 +928,14 @@ function all(results) {
|
|
|
791
928
|
* Same folding rules as {@link all}: first `Err` short-circuits, any `Defect`
|
|
792
929
|
* dominates. This is **not** error accumulation.
|
|
793
930
|
*
|
|
931
|
+
* @category Aggregate
|
|
932
|
+
*
|
|
794
933
|
* @example
|
|
795
934
|
* ```ts
|
|
796
|
-
* import { allFromDict, Ok } from "unthrown";
|
|
797
|
-
*
|
|
935
|
+
* import { allFromDict, Ok, Err } from "unthrown";
|
|
936
|
+
*
|
|
937
|
+
* allFromDict({ id: Ok(1), name: Ok("ada") }).unwrap(); // => { id: 1, name: "ada" }
|
|
938
|
+
* allFromDict({ id: Ok(1), name: Err("missing") }); // => Err("missing")
|
|
798
939
|
* ```
|
|
799
940
|
*/
|
|
800
941
|
function allFromDict(results) {
|
|
@@ -810,10 +951,14 @@ function allFromDict(results) {
|
|
|
810
951
|
* short-circuits, any `Defect` dominates. As ever, the returned `AsyncResult`'s
|
|
811
952
|
* internal promise never rejects. For a **record**, use {@link allFromDictAsync}.
|
|
812
953
|
*
|
|
954
|
+
* @category Aggregate
|
|
955
|
+
*
|
|
813
956
|
* @example
|
|
814
957
|
* ```ts
|
|
815
958
|
* import { allAsync, fromSafePromise } from "unthrown";
|
|
816
|
-
*
|
|
959
|
+
*
|
|
960
|
+
* const both = allAsync([fromSafePromise(Promise.resolve(1)), fromSafePromise(Promise.resolve(2))]);
|
|
961
|
+
* (await both).unwrap(); // => [1, 2]
|
|
817
962
|
* ```
|
|
818
963
|
*/
|
|
819
964
|
function allAsync(results) {
|
|
@@ -827,10 +972,17 @@ function allAsync(results) {
|
|
|
827
972
|
* Resolved concurrently (order preserved), folded with the {@link all} rules,
|
|
828
973
|
* and the internal promise never rejects.
|
|
829
974
|
*
|
|
975
|
+
* @category Aggregate
|
|
976
|
+
*
|
|
830
977
|
* @example
|
|
831
978
|
* ```ts
|
|
832
979
|
* import { allFromDictAsync, fromSafePromise } from "unthrown";
|
|
833
|
-
*
|
|
980
|
+
*
|
|
981
|
+
* const both = allFromDictAsync({
|
|
982
|
+
* a: fromSafePromise(Promise.resolve(1)),
|
|
983
|
+
* b: fromSafePromise(Promise.resolve("x")),
|
|
984
|
+
* });
|
|
985
|
+
* (await both).unwrap(); // => { a: 1, b: "x" }
|
|
834
986
|
* ```
|
|
835
987
|
*/
|
|
836
988
|
function allFromDictAsync(results) {
|
|
@@ -862,10 +1014,12 @@ function allFromDictAsync(results) {
|
|
|
862
1014
|
* (`AsyncResult.fromPromise`, `AsyncResult.all`, …), grouped by what they
|
|
863
1015
|
* return — a static lives in exactly one namespace.
|
|
864
1016
|
*
|
|
1017
|
+
* @category Facade
|
|
1018
|
+
*
|
|
865
1019
|
* @example
|
|
866
1020
|
* ```ts
|
|
867
1021
|
* import { Result } from "unthrown";
|
|
868
|
-
* Result.Ok(1).flatMap((n) => Result.Ok(n + 1)).unwrap(); // 2
|
|
1022
|
+
* Result.Ok(1).flatMap((n) => Result.Ok(n + 1)).unwrap(); // => 2
|
|
869
1023
|
* ```
|
|
870
1024
|
*/
|
|
871
1025
|
const Result = {
|
|
@@ -896,10 +1050,13 @@ const Result = {
|
|
|
896
1050
|
* {@link Result}, the free functions remain the primary, tree-shakeable API; the
|
|
897
1051
|
* value `AsyncResult` and the type {@link AsyncResult} share one name.
|
|
898
1052
|
*
|
|
1053
|
+
* @category Facade
|
|
1054
|
+
*
|
|
899
1055
|
* @example
|
|
900
1056
|
* ```ts
|
|
901
1057
|
* import { AsyncResult } from "unthrown";
|
|
902
1058
|
* const user = await AsyncResult.fromPromise(fetchUser(id), (c, defect) => defect(c));
|
|
1059
|
+
* user.unwrap(); // => the fetched user (on success)
|
|
903
1060
|
* ```
|
|
904
1061
|
*/
|
|
905
1062
|
const AsyncResult = {
|
|
@@ -916,9 +1073,17 @@ const AsyncResult = {
|
|
|
916
1073
|
*
|
|
917
1074
|
* @remarks
|
|
918
1075
|
* Extend the returned class to declare a concrete error. Supply the payload with
|
|
919
|
-
* an instantiation expression; omit it for a payload-less error.
|
|
920
|
-
*
|
|
921
|
-
*
|
|
1076
|
+
* an instantiation expression; omit it for a payload-less error. The `message`
|
|
1077
|
+
* is **not** a payload field — it is the human string owned by `Error`, not
|
|
1078
|
+
* structured data, so it is reserved. Define it once per subclass the standard
|
|
1079
|
+
* way, `override message = "…"` (it may interpolate the payload via `this`,
|
|
1080
|
+
* which the base populates before the subclass field initialiser runs); a
|
|
1081
|
+
* payload `message` is rejected at compile time, so contextual detail lives in
|
|
1082
|
+
* typed fields, never baked into per-call prose. The `_tag` always reflects
|
|
1083
|
+
* `tag` and cannot be overridden by the payload. `name` is likewise reserved —
|
|
1084
|
+
* it is the display label (set it with `options.name`); a payload `name` is
|
|
1085
|
+
* rejected at compile time (and excluded from the instance type), so it can't
|
|
1086
|
+
* shadow `Error.name`.
|
|
922
1087
|
*
|
|
923
1088
|
* `_tag` is the discriminant used by {@link matchTags}; `Error.name` is the
|
|
924
1089
|
* human-facing label in stack traces and logs. By default they coincide, but
|
|
@@ -929,11 +1094,14 @@ const AsyncResult = {
|
|
|
929
1094
|
* ```ts
|
|
930
1095
|
* class RetryableError extends TaggedError("@my-lib/RetryableError", {
|
|
931
1096
|
* name: "RetryableError",
|
|
932
|
-
* })
|
|
1097
|
+
* }) {
|
|
1098
|
+
* override message = "operation failed; safe to retry";
|
|
1099
|
+
* }
|
|
933
1100
|
*
|
|
934
|
-
* const e = new RetryableError(
|
|
935
|
-
* e._tag;
|
|
936
|
-
* e.name;
|
|
1101
|
+
* const e = new RetryableError();
|
|
1102
|
+
* e._tag; // "@my-lib/RetryableError" — namespaced discriminant
|
|
1103
|
+
* e.name; // "RetryableError" — clean display name
|
|
1104
|
+
* e.message; // "operation failed; safe to retry" — the standard Error.message
|
|
937
1105
|
* ```
|
|
938
1106
|
*
|
|
939
1107
|
* @typeParam Tag - the string literal discriminant.
|
|
@@ -941,13 +1109,15 @@ const AsyncResult = {
|
|
|
941
1109
|
* @param options - optional overrides. `options.name` sets `Error.name`
|
|
942
1110
|
* independently of `tag` (defaults to `tag`).
|
|
943
1111
|
*
|
|
1112
|
+
* @category Tagged errors
|
|
1113
|
+
*
|
|
944
1114
|
* @example
|
|
945
1115
|
* ```ts
|
|
946
1116
|
* class NotFound extends TaggedError("NotFound") {}
|
|
947
1117
|
* class HttpError extends TaggedError("HttpError")<{ status: number }> {}
|
|
948
1118
|
*
|
|
949
|
-
* new NotFound()._tag; // "NotFound"
|
|
950
|
-
* new HttpError({ status: 500 }).status; // 500
|
|
1119
|
+
* new NotFound()._tag; // => "NotFound"
|
|
1120
|
+
* new HttpError({ status: 500 }).status; // => 500
|
|
951
1121
|
* ```
|
|
952
1122
|
*/
|
|
953
1123
|
function TaggedError(tag, options) {
|
|
@@ -955,10 +1125,11 @@ function TaggedError(tag, options) {
|
|
|
955
1125
|
class TaggedErrorBase extends Error {
|
|
956
1126
|
_tag;
|
|
957
1127
|
constructor(props) {
|
|
958
|
-
super(
|
|
1128
|
+
super();
|
|
959
1129
|
if (props) Object.assign(this, props);
|
|
960
1130
|
this._tag = tag;
|
|
961
1131
|
this.name = displayName;
|
|
1132
|
+
delete this.message;
|
|
962
1133
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
963
1134
|
}
|
|
964
1135
|
}
|
|
@@ -966,8 +1137,9 @@ function TaggedError(tag, options) {
|
|
|
966
1137
|
}
|
|
967
1138
|
function matchTags(result, handlers) {
|
|
968
1139
|
const onErr = (error) => {
|
|
969
|
-
const
|
|
970
|
-
|
|
1140
|
+
const tag = error._tag;
|
|
1141
|
+
const handler = tag === "Ok" || tag === "Defect" || !Object.hasOwn(handlers, tag) ? void 0 : handlers[tag];
|
|
1142
|
+
return handler ? handler(error) : handlers.Defect(error);
|
|
971
1143
|
};
|
|
972
1144
|
return result.match({
|
|
973
1145
|
ok: handlers.Ok,
|