unthrown 5.9.0 → 5.11.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 +0 -3
- package/dist/index.cjs +268 -60
- package/dist/index.d.cts +1768 -1624
- package/dist/index.d.mts +1768 -1624
- package/dist/index.mjs +268 -60
- package/package.json +6 -6
package/dist/index.mjs
CHANGED
|
@@ -17,24 +17,63 @@ const PATTERN_BRAND = Symbol.for("unthrown.matcher.pattern");
|
|
|
17
17
|
* is a bug).
|
|
18
18
|
*
|
|
19
19
|
* @category Errors
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { match, NonExhaustiveError } from "unthrown";
|
|
24
|
+
*
|
|
25
|
+
* // A value typed "a" | "b" that is really "c" (a cast, a raw-JS caller):
|
|
26
|
+
* const rogue = "c" as "a" | "b";
|
|
27
|
+
* try {
|
|
28
|
+
* match(rogue)
|
|
29
|
+
* .with("a", () => 1)
|
|
30
|
+
* .with("b", () => 2)
|
|
31
|
+
* .exhaustive();
|
|
32
|
+
* } catch (error) {
|
|
33
|
+
* error instanceof NonExhaustiveError; // => true
|
|
34
|
+
* (error as NonExhaustiveError).input; // => "c"
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
20
37
|
*/
|
|
21
38
|
var NonExhaustiveError = class extends Error {
|
|
22
39
|
/** The value no arm matched. */
|
|
23
40
|
input;
|
|
24
41
|
constructor(input) {
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
printed = JSON.stringify(input) ?? String(input);
|
|
28
|
-
} catch {
|
|
29
|
-
printed = String(input);
|
|
30
|
-
}
|
|
31
|
-
super(`unthrown: no pattern matched the value ${printed}`);
|
|
42
|
+
super(`unthrown: no pattern matched the value ${printValue(input)}`);
|
|
32
43
|
this.name = "NonExhaustiveError";
|
|
33
44
|
this.input = input;
|
|
34
45
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
35
46
|
}
|
|
36
47
|
};
|
|
37
48
|
/**
|
|
49
|
+
* Render a rogue value for {@link NonExhaustiveError}'s message — **total**,
|
|
50
|
+
* because the error is constructed inside the throw → defect net and a throw
|
|
51
|
+
* here would replace the diagnostic with an unrelated one (or escape `match`).
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* Each step can fail for a different input, so each is guarded: `JSON.stringify`
|
|
55
|
+
* RETURNS undefined for a function, a symbol or `undefined` and throws for a
|
|
56
|
+
* bigint or a circular object; `String()` throws for a null-prototype object or
|
|
57
|
+
* a hostile `toString` / `Symbol.toPrimitive`; `Object.prototype.toString`
|
|
58
|
+
* throws only for a Proxy whose `get` trap does. The last resort is a constant.
|
|
59
|
+
*
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
function printValue(input) {
|
|
63
|
+
try {
|
|
64
|
+
const json = JSON.stringify(input);
|
|
65
|
+
if (json !== void 0) return json;
|
|
66
|
+
} catch {}
|
|
67
|
+
try {
|
|
68
|
+
return String(input);
|
|
69
|
+
} catch {}
|
|
70
|
+
try {
|
|
71
|
+
return Object.prototype.toString.call(input);
|
|
72
|
+
} catch {
|
|
73
|
+
return "<unprintable value>";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
38
77
|
* Is `x` a *plain* object (prototype `Object.prototype` or `null`) — an object
|
|
39
78
|
* literal, the only object shape that acts as a structural pattern?
|
|
40
79
|
*
|
|
@@ -122,6 +161,24 @@ Object.freeze(MatcherImpl.prototype);
|
|
|
122
161
|
* {@link P}).
|
|
123
162
|
*
|
|
124
163
|
* @category Constructors
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```ts
|
|
167
|
+
* import { match, type Result } from "unthrown";
|
|
168
|
+
*
|
|
169
|
+
* // Matching a whole Result natively — every variant named, `.exhaustive()` last:
|
|
170
|
+
* declare const r: Result<number, "odd" | "negative">;
|
|
171
|
+
* const label = match(r)
|
|
172
|
+
* .with({ tag: "Ok" }, (ok) => `got ${ok.value}`)
|
|
173
|
+
* .with({ tag: "Err" }, (err) => `failed: ${err.error}`)
|
|
174
|
+
* .with({ tag: "Defect" }, () => "bug")
|
|
175
|
+
* .exhaustive();
|
|
176
|
+
*
|
|
177
|
+
* // Inside a combinator, return the un-terminated builder — it runs `.exhaustive()`:
|
|
178
|
+
* const reason = r.mapErrCases((matcher) =>
|
|
179
|
+
* matcher.with("odd", () => "not even" as const).with("negative", () => "below zero" as const),
|
|
180
|
+
* ); // Result<number, "not even" | "below zero">
|
|
181
|
+
* ```
|
|
125
182
|
*/
|
|
126
183
|
function match(value) {
|
|
127
184
|
return new MatcherImpl(value);
|
|
@@ -155,17 +212,49 @@ const universal = pattern(() => true);
|
|
|
155
212
|
* (`.with(P.tag("A"), P.tag("B"), handler)`).
|
|
156
213
|
* - `P.instanceOf(Cls)` — an `instanceof` check, narrowing to the class
|
|
157
214
|
* instance type (for union members that are not tagged, e.g. a third-party
|
|
158
|
-
* error class).
|
|
215
|
+
* error class). **Exhaustiveness here is structural, the check is not:**
|
|
216
|
+
* two classes with the same shape (`class A extends Error {}`,
|
|
217
|
+
* `class B extends Error {}`) are one type to the compiler, so a match
|
|
218
|
+
* naming only `A` compiles as exhaustive while a `B` fails `instanceof A`
|
|
219
|
+
* at runtime and becomes a `Defect`. Give each class a distinguishing field
|
|
220
|
+
* (a `readonly kind = "A"` literal) or use `TaggedError`, and the missing
|
|
221
|
+
* arm is a compile error again.
|
|
159
222
|
* - `P.when(guard)` — an arbitrary type-guard predicate. Also the way to match
|
|
160
223
|
* a primitive shape (`P.when((v): v is string => typeof v === "string")`),
|
|
161
224
|
* and grouping patterns under one handler is what a `.with(a, b, handler)`
|
|
162
225
|
* arm already does.
|
|
163
226
|
*
|
|
164
227
|
* @category Constructors
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```ts
|
|
231
|
+
* import { P, TaggedError, type Result } from "unthrown";
|
|
232
|
+
*
|
|
233
|
+
* class NotFound extends TaggedError("NotFound")<{ id: string }> {}
|
|
234
|
+
* class Conflict extends TaggedError("Conflict") {}
|
|
235
|
+
* class VendorTimeout extends Error {
|
|
236
|
+
* readonly afterMs = 30_000;
|
|
237
|
+
* }
|
|
238
|
+
*
|
|
239
|
+
* declare const r: Result<string, NotFound | Conflict | VendorTimeout | "rate_limited">;
|
|
240
|
+
* const status = r.match({
|
|
241
|
+
* ok: () => 200,
|
|
242
|
+
* errCases: (matcher) =>
|
|
243
|
+
* matcher
|
|
244
|
+
* .with(P.tag("NotFound"), () => 404) // a TaggedError, narrowed with its payload
|
|
245
|
+
* .with(P.tag("Conflict"), () => 409)
|
|
246
|
+
* .with(P.instanceOf(VendorTimeout), (e) => (e.afterMs > 10_000 ? 504 : 503))
|
|
247
|
+
* .with(
|
|
248
|
+
* P.when((v): v is "rate_limited" => v === "rate_limited"),
|
|
249
|
+
* () => 429,
|
|
250
|
+
* ),
|
|
251
|
+
* defect: () => 500,
|
|
252
|
+
* });
|
|
253
|
+
* ```
|
|
165
254
|
*/
|
|
166
255
|
const P = Object.freeze({
|
|
167
256
|
_: universal,
|
|
168
|
-
tag: (value) => ({ _tag: value }),
|
|
257
|
+
tag: (value) => Object.freeze({ _tag: value }),
|
|
169
258
|
instanceOf: (cls) => pattern((value) => value instanceof cls),
|
|
170
259
|
when: (guard) => pattern(guard)
|
|
171
260
|
});
|
|
@@ -327,6 +416,7 @@ var Res = class {
|
|
|
327
416
|
try {
|
|
328
417
|
const out = runMatch(f, this.error);
|
|
329
418
|
if (isDefectMarker(out)) return defectRes(out.cause);
|
|
419
|
+
if (isThenable(out)) return asyncBranchDefect(out);
|
|
330
420
|
return errRes(out);
|
|
331
421
|
} catch (cause) {
|
|
332
422
|
return defectRes(cause);
|
|
@@ -348,6 +438,7 @@ var Res = class {
|
|
|
348
438
|
try {
|
|
349
439
|
const out = runMatch(f, this.error);
|
|
350
440
|
if (isDefectMarker(out)) return defectRes(out.cause);
|
|
441
|
+
if (isThenable(out)) return asyncBranchDefect(out);
|
|
351
442
|
return okRes(out);
|
|
352
443
|
} catch (cause) {
|
|
353
444
|
return defectRes(cause);
|
|
@@ -517,7 +608,8 @@ function defectRes(cause) {
|
|
|
517
608
|
* brand the prototype carries — so a `Result` built by **another copy** of
|
|
518
609
|
* unthrown, e.g. the CJS and ESM builds loaded side by side, is still
|
|
519
610
|
* recognised). A look-alike plain object (`{ tag: "Ok" }`) carries neither and
|
|
520
|
-
* is **not** matched
|
|
611
|
+
* is **not** matched; nor is a forgery built on the real prototype whose `tag` or
|
|
612
|
+
* payload is a getter (both must be own data properties). An `AsyncResult` is not a `Result` and returns `false`.
|
|
521
613
|
*
|
|
522
614
|
* @returns `true` when `x` is a `Result` produced by this library.
|
|
523
615
|
*
|
|
@@ -544,13 +636,42 @@ function defectRes(cause) {
|
|
|
544
636
|
* @category Guards
|
|
545
637
|
*/
|
|
546
638
|
function isResult(x) {
|
|
547
|
-
if (x instanceof Res) return true;
|
|
548
639
|
try {
|
|
549
|
-
return (typeof x === "object" || typeof x === "function") && x !== null && Reflect.get(x, RESULT_BRAND) === true;
|
|
640
|
+
return (x instanceof Res || (typeof x === "object" || typeof x === "function") && x !== null && Reflect.get(x, RESULT_BRAND) === true) && hasOwnVariantShape(x);
|
|
550
641
|
} catch {
|
|
551
642
|
return false;
|
|
552
643
|
}
|
|
553
644
|
}
|
|
645
|
+
/** Each variant's payload key. @internal */
|
|
646
|
+
const PAYLOAD_KEY = {
|
|
647
|
+
Ok: "value",
|
|
648
|
+
Err: "error",
|
|
649
|
+
Defect: "cause"
|
|
650
|
+
};
|
|
651
|
+
/**
|
|
652
|
+
* Does `x` carry a variant's shape as own **data** properties — `tag` one of
|
|
653
|
+
* the three variants, plus that variant's payload key?
|
|
654
|
+
*
|
|
655
|
+
* @remarks
|
|
656
|
+
* A brand is not enough: the prototype (and so the brand) is reachable from any
|
|
657
|
+
* genuine `Result`, so `Object.create(protoOf(Ok(1)))` with a throwing `tag` or
|
|
658
|
+
* payload getter passed the guard and then threw — raw out of `all`, or as a
|
|
659
|
+
* rejection out of an `AsyncResult` that must never reject. Every genuine
|
|
660
|
+
* `Result`, from any copy of the library, is a frozen object literal whose `tag`
|
|
661
|
+
* and payload are own data properties, so reading their descriptors (which
|
|
662
|
+
* never runs a getter) accepts all of them and no getter-bearing forgery.
|
|
663
|
+
*
|
|
664
|
+
* @internal
|
|
665
|
+
*/
|
|
666
|
+
function hasOwnVariantShape(x) {
|
|
667
|
+
const tag = Object.getOwnPropertyDescriptor(x, "tag");
|
|
668
|
+
if (tag === void 0 || !("value" in tag)) return false;
|
|
669
|
+
const variant = tag.value;
|
|
670
|
+
if (variant !== "Ok" && variant !== "Err" && variant !== "Defect") return false;
|
|
671
|
+
const key = PAYLOAD_KEY[variant];
|
|
672
|
+
const payload = Object.getOwnPropertyDescriptor(x, key);
|
|
673
|
+
return payload !== void 0 && "value" in payload;
|
|
674
|
+
}
|
|
554
675
|
/**
|
|
555
676
|
* Reuse a non-matching variant (an `Err` or `Defect`) as a differently-typed
|
|
556
677
|
* `Result`, with no runtime work. Sound because the passed-through variant
|
|
@@ -576,9 +697,9 @@ function passThrough(self) {
|
|
|
576
697
|
* - inside a `try` that routes the throw to a `Defect` (the boundaries in
|
|
577
698
|
* `interop.ts`, where a hostile value arriving at a triage point *is* an
|
|
578
699
|
* unmodeled failure and should surface as one); or
|
|
579
|
-
* -
|
|
580
|
-
*
|
|
581
|
-
* drop it without throwing.
|
|
700
|
+
* - to classify a value that is then **discarded** (a `Defect` minted, the
|
|
701
|
+
* value handed to {@link silenceIfThenable}), where the only correct answer
|
|
702
|
+
* is to drop it without throwing — and without starting a lazy thenable.
|
|
582
703
|
*
|
|
583
704
|
* Calling it bare, outside both, is a bug: the throw escapes into whatever
|
|
584
705
|
* context invoked it.
|
|
@@ -589,27 +710,37 @@ function isThenable(x) {
|
|
|
589
710
|
return (typeof x === "object" || typeof x === "function") && x !== null && typeof x.then === "function";
|
|
590
711
|
}
|
|
591
712
|
/**
|
|
592
|
-
*
|
|
713
|
+
* Silence a **genuine `Promise`** a combinator or boundary is about to
|
|
714
|
+
* **discard** — and leave every other thenable untouched.
|
|
593
715
|
*
|
|
594
716
|
* @remarks
|
|
595
717
|
* The observers (`tap`, `tapErrCases`, `tapDefect`, `tapFailure`) throw their
|
|
596
718
|
* callback's return value away, and the `Result`-returning combinators reject a
|
|
597
|
-
* non-`Result` one. Either way, a
|
|
719
|
+
* non-`Result` one. Either way, a promise that slipped past `NotThenable` (a
|
|
598
720
|
* cast, a raw-JS caller) is dropped while still in flight — and if it later
|
|
599
721
|
* rejects, nothing is holding it, so the rejection floats unhandled and takes
|
|
600
722
|
* the process down on Node by default. Worse for an observer: its whole job is
|
|
601
723
|
* to make a failure visible, and this is the one path where the failure is
|
|
602
|
-
* invisible.
|
|
724
|
+
* invisible. Attaching a no-op rejection handler costs nothing and changes no
|
|
725
|
+
* outcome.
|
|
603
726
|
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
727
|
+
* Only a `Promise` **instance** is touched. A promise is already running, so
|
|
728
|
+
* handling its rejection starts nothing; a non-`Promise` thenable may be
|
|
729
|
+
* **lazy** — a `PrismaPromise`, a query builder — whose work begins only when
|
|
730
|
+
* `then` is called. Adopting one (`Promise.resolve(x)` calls `x.then`) would
|
|
731
|
+
* *run* the effect the caller is being told was refused: `fromSafeThrowable(()
|
|
732
|
+
* => prisma.user.deleteMany())` returned a `Defect` and deleted the rows
|
|
733
|
+
* anyway. A lazy thenable that is never started cannot reject, so there is
|
|
734
|
+
* nothing to silence. Callers still *classify* any thenable (a `Defect` where
|
|
735
|
+
* the spec says so) via {@link isThenable}; this only decides what to adopt.
|
|
736
|
+
*
|
|
737
|
+
* Total: a hostile `then` getter or `Symbol.hasInstance` path is swallowed.
|
|
607
738
|
*
|
|
608
739
|
* @internal
|
|
609
740
|
*/
|
|
610
741
|
function silenceIfThenable(value) {
|
|
611
742
|
try {
|
|
612
|
-
if (
|
|
743
|
+
if (value instanceof Promise) value.then(void 0, () => void 0);
|
|
613
744
|
} catch {}
|
|
614
745
|
}
|
|
615
746
|
/**
|
|
@@ -627,6 +758,19 @@ function nonResultCallbackDefect(returned) {
|
|
|
627
758
|
return defectRes(/* @__PURE__ */ new TypeError("unthrown: a combinator callback returned a non-Result value"));
|
|
628
759
|
}
|
|
629
760
|
/**
|
|
761
|
+
* The Defect minted when a non-awaiting error transformer (`mapErrCases` /
|
|
762
|
+
* `recoverErrCases`) gets a thenable branch output past the compile-time ban
|
|
763
|
+
* (a cast, an untyped caller): never `Err(<Promise>)` / `Ok(<Promise>)` — a
|
|
764
|
+
* Promise in the channel is un-triaged — and a genuine Promise is silenced so
|
|
765
|
+
* its rejection cannot float. The sibling of the aggregates' async-`merge` net.
|
|
766
|
+
*
|
|
767
|
+
* @internal
|
|
768
|
+
*/
|
|
769
|
+
function asyncBranchDefect(out) {
|
|
770
|
+
silenceIfThenable(out);
|
|
771
|
+
return defectRes(/* @__PURE__ */ new TypeError("unthrown: mapErrCases/recoverErrCases branches must be SYNCHRONOUS, but one returned a thenable — lift async work with fromPromise and use flatMapErrCases"));
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
630
774
|
* Drive an error-combinator callback: build `match(error)`, hand it (plus the
|
|
631
775
|
* injected `defect`) to the callback, and `.run()` the returned exhaustive
|
|
632
776
|
* builder to its output. `.run()` executes `.exhaustive()` — type-forced
|
|
@@ -675,10 +819,23 @@ function observerThrowToDefect(thrown, original) {
|
|
|
675
819
|
* @internal
|
|
676
820
|
*/
|
|
677
821
|
function scopeOf(value) {
|
|
678
|
-
if (typeof value !== "object" || value === null ||
|
|
822
|
+
if (typeof value !== "object" || value === null || !isPlainScope(value)) throw new TypeError("bind/let requires a plain object scope — start a do-chain with Do()");
|
|
679
823
|
return value;
|
|
680
824
|
}
|
|
681
825
|
/**
|
|
826
|
+
* A *plain* object: its prototype is `null` or an `Object.prototype` (any
|
|
827
|
+
* realm's — recognised by having no prototype of its own). The spread that
|
|
828
|
+
* merges a `bind`/`let` key copies only own enumerable data, so a class
|
|
829
|
+
* instance's getters and prototype methods — and an array's identity — would
|
|
830
|
+
* silently vanish while the type still claimed them.
|
|
831
|
+
*
|
|
832
|
+
* @internal
|
|
833
|
+
*/
|
|
834
|
+
function isPlainScope(value) {
|
|
835
|
+
const proto = Object.getPrototypeOf(value);
|
|
836
|
+
return proto === null || Object.getPrototypeOf(proto) === null;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
682
839
|
* The sole runtime implementation of {@link AsyncResult}: wraps a
|
|
683
840
|
* `Promise<Result>` constructed never to reject. Operates on the public `Result`
|
|
684
841
|
* union (via `tag`), never on `Res` internals. Never re-exported from `index.ts`.
|
|
@@ -768,7 +925,7 @@ var AsyncRes = class AsyncRes {
|
|
|
768
925
|
ensure(predicate, onFail) {
|
|
769
926
|
return this.#lift((r) => r.ensure(predicate, onFail));
|
|
770
927
|
}
|
|
771
|
-
mapErrCases(f) {
|
|
928
|
+
mapErrCases(f, ..._guard) {
|
|
772
929
|
return this.#lift((r) => r.mapErrCases(f));
|
|
773
930
|
}
|
|
774
931
|
flatMapErrCases(f) {
|
|
@@ -785,7 +942,7 @@ var AsyncRes = class AsyncRes {
|
|
|
785
942
|
}
|
|
786
943
|
}));
|
|
787
944
|
}
|
|
788
|
-
recoverErrCases(f) {
|
|
945
|
+
recoverErrCases(f, ..._guard) {
|
|
789
946
|
return this.#lift((r) => r.recoverErrCases(f));
|
|
790
947
|
}
|
|
791
948
|
tapErrCases(f) {
|
|
@@ -1123,12 +1280,17 @@ function fromNullable(value, onAbsent) {
|
|
|
1123
1280
|
function fromThrowable(fn, qualify) {
|
|
1124
1281
|
const triage = qualify;
|
|
1125
1282
|
return (...args) => {
|
|
1283
|
+
let value;
|
|
1126
1284
|
try {
|
|
1127
|
-
|
|
1128
|
-
return isThenable(value) ? thenableReturnDefect(value, SYNC_FN_THENABLE) : Ok(value);
|
|
1285
|
+
value = fn(...args);
|
|
1129
1286
|
} catch (cause) {
|
|
1130
1287
|
return qualifyToResult(cause, triage);
|
|
1131
1288
|
}
|
|
1289
|
+
try {
|
|
1290
|
+
return isThenable(value) ? thenableReturnDefect(value, SYNC_FN_THENABLE) : Ok(value);
|
|
1291
|
+
} catch (cause) {
|
|
1292
|
+
return defectRes(cause);
|
|
1293
|
+
}
|
|
1132
1294
|
};
|
|
1133
1295
|
}
|
|
1134
1296
|
/**
|
|
@@ -1310,7 +1472,7 @@ function fromExecutor(executor) {
|
|
|
1310
1472
|
};
|
|
1311
1473
|
try {
|
|
1312
1474
|
const returned = executor(settle, defect);
|
|
1313
|
-
if (
|
|
1475
|
+
if (returned instanceof Promise) returned.then(void 0, (cause) => settle(defect(cause)));
|
|
1314
1476
|
} catch (cause) {
|
|
1315
1477
|
settle(defect(cause));
|
|
1316
1478
|
}
|
|
@@ -1321,7 +1483,7 @@ function qualifyToResult(cause, qualify) {
|
|
|
1321
1483
|
const q = qualify(cause, defect);
|
|
1322
1484
|
if (isDefectMarker(q)) return defectRes(q.cause);
|
|
1323
1485
|
if (isThenable(q)) {
|
|
1324
|
-
|
|
1486
|
+
silenceIfThenable(q);
|
|
1325
1487
|
return defectRes(/* @__PURE__ */ new TypeError("unthrown: qualify must be synchronous — it returned a thenable; triage the cause without awaiting"));
|
|
1326
1488
|
}
|
|
1327
1489
|
return errRes(q);
|
|
@@ -1362,7 +1524,7 @@ const MERGE_THENABLE = "unthrown: an accumulating aggregate's `merge` must be SY
|
|
|
1362
1524
|
* collapses to `unknown`. (The phantom rest-tuple guard `fromPromise` uses fares
|
|
1363
1525
|
* worse.) `merge` *is* `NotThenable`-constrained, but a cast or an untyped
|
|
1364
1526
|
* caller still reaches here. Either way the runtime answer is the same, and it
|
|
1365
|
-
* costs nothing: a Defect, plus
|
|
1527
|
+
* costs nothing: a Defect, plus a no-op rejection handler so an orphaned Promise
|
|
1366
1528
|
* cannot float.
|
|
1367
1529
|
*
|
|
1368
1530
|
* @internal
|
|
@@ -1372,6 +1534,38 @@ function thenableReturnDefect(value, message) {
|
|
|
1372
1534
|
return defectRes(new TypeError(message));
|
|
1373
1535
|
}
|
|
1374
1536
|
/**
|
|
1537
|
+
* A record's own **enumerable** keys — strings and symbols — with their values.
|
|
1538
|
+
*
|
|
1539
|
+
* @remarks
|
|
1540
|
+
* `Object.keys` / `Object.values` skip symbol keys, so a symbol-keyed `Err`
|
|
1541
|
+
* silently vanished from the fold while the types (`keyof R` includes it)
|
|
1542
|
+
* promised otherwise. `Reflect.ownKeys` filtered to the enumerable ones is
|
|
1543
|
+
* `Object.keys` plus symbols, in the same order (strings first, then symbols).
|
|
1544
|
+
* May throw on an out-of-contract container (`null`, a throwing getter) — every
|
|
1545
|
+
* caller routes that to a `Defect`.
|
|
1546
|
+
*
|
|
1547
|
+
* @internal
|
|
1548
|
+
*/
|
|
1549
|
+
function ownEntries(record) {
|
|
1550
|
+
const keys = Reflect.ownKeys(record).filter((key) => Object.prototype.propertyIsEnumerable.call(record, key));
|
|
1551
|
+
return [keys, keys.map((key) => record[key])];
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Build an async aggregate's settled promise, turning a synchronous throw while
|
|
1555
|
+
* reading an out-of-contract container (`allAsync(undefined)`, a throwing
|
|
1556
|
+
* getter) into a `Defect` — the returned `AsyncResult` still never rejects and
|
|
1557
|
+
* the call never throws.
|
|
1558
|
+
*
|
|
1559
|
+
* @internal
|
|
1560
|
+
*/
|
|
1561
|
+
function settleOrDefect(build) {
|
|
1562
|
+
try {
|
|
1563
|
+
return new AsyncRes(build());
|
|
1564
|
+
} catch (cause) {
|
|
1565
|
+
return new AsyncRes(Promise.resolve(defectRes(cause)));
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
/**
|
|
1375
1569
|
* Fold an array of settled `Result`s: first `Err` wins, any `Defect` dominates,
|
|
1376
1570
|
* else `Ok` of the values array.
|
|
1377
1571
|
*
|
|
@@ -1393,31 +1587,34 @@ function settleAll(results) {
|
|
|
1393
1587
|
return Promise.all(results.map((r) => Promise.resolve(r).then((x) => x, (cause) => defectRes(cause))));
|
|
1394
1588
|
}
|
|
1395
1589
|
function foldArray(results, merge) {
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1590
|
+
try {
|
|
1591
|
+
let firstErr;
|
|
1592
|
+
let firstDefect;
|
|
1593
|
+
const values = [];
|
|
1594
|
+
const errors = [];
|
|
1595
|
+
for (const [i, r] of results.entries()) {
|
|
1596
|
+
if (!isResult(r)) {
|
|
1597
|
+
firstDefect ??= nonResultDefect();
|
|
1598
|
+
break;
|
|
1599
|
+
}
|
|
1600
|
+
if (r.tag === "Defect") {
|
|
1601
|
+
firstDefect ??= r;
|
|
1602
|
+
break;
|
|
1603
|
+
} else if (r.tag === "Err") {
|
|
1604
|
+
if (merge) errors.push([i, r.error]);
|
|
1605
|
+
else firstErr ??= r;
|
|
1606
|
+
} else values.push(r.value);
|
|
1404
1607
|
}
|
|
1405
|
-
if (
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
if (firstDefect) return firstDefect;
|
|
1413
|
-
if (merge && errors.length > 0) try {
|
|
1414
|
-
const merged = merge(errors);
|
|
1415
|
-
if (isThenable(merged)) return thenableReturnDefect(merged, MERGE_THENABLE);
|
|
1416
|
-
return Err(merged);
|
|
1608
|
+
if (firstDefect) return firstDefect;
|
|
1609
|
+
if (merge && errors.length > 0) {
|
|
1610
|
+
const merged = merge(errors);
|
|
1611
|
+
if (isThenable(merged)) return thenableReturnDefect(merged, MERGE_THENABLE);
|
|
1612
|
+
return Err(merged);
|
|
1613
|
+
}
|
|
1614
|
+
return firstErr ?? Ok(values);
|
|
1417
1615
|
} catch (cause) {
|
|
1418
1616
|
return defectRes(cause);
|
|
1419
1617
|
}
|
|
1420
|
-
return firstErr ?? Ok(values);
|
|
1421
1618
|
}
|
|
1422
1619
|
/**
|
|
1423
1620
|
* Fold a record of settled `Result`s with the same rules, else `Ok` of the
|
|
@@ -1436,8 +1633,14 @@ function foldArray(results, merge) {
|
|
|
1436
1633
|
* @internal
|
|
1437
1634
|
*/
|
|
1438
1635
|
function foldRecord(results, merge) {
|
|
1439
|
-
|
|
1440
|
-
|
|
1636
|
+
let keys;
|
|
1637
|
+
let values;
|
|
1638
|
+
try {
|
|
1639
|
+
[keys, values] = ownEntries(results);
|
|
1640
|
+
} catch (cause) {
|
|
1641
|
+
return defectRes(cause);
|
|
1642
|
+
}
|
|
1643
|
+
return foldArray(values, merge && ((errors) => merge(nameErrors(errors, keys)))).map((values) => Object.fromEntries(keys.map((key, i) => [key, values[i]])));
|
|
1441
1644
|
}
|
|
1442
1645
|
/** Drop the accumulated indices — the positional forms merge errors alone. @internal */
|
|
1443
1646
|
function stripIndices(errors) {
|
|
@@ -1523,7 +1726,7 @@ function allFromDict(results) {
|
|
|
1523
1726
|
* ```
|
|
1524
1727
|
*/
|
|
1525
1728
|
function allAsync(results) {
|
|
1526
|
-
return
|
|
1729
|
+
return settleOrDefect(() => settleAll(results).then((resolved) => foldArray(resolved)));
|
|
1527
1730
|
}
|
|
1528
1731
|
/**
|
|
1529
1732
|
* The asynchronous counterpart of {@link allFromDict}: combine a record of
|
|
@@ -1548,8 +1751,10 @@ function allAsync(results) {
|
|
|
1548
1751
|
* ```
|
|
1549
1752
|
*/
|
|
1550
1753
|
function allFromDictAsync(results) {
|
|
1551
|
-
|
|
1552
|
-
|
|
1754
|
+
return settleOrDefect(() => {
|
|
1755
|
+
const [keys, values] = ownEntries(results);
|
|
1756
|
+
return settleAll(values).then((resolved) => foldRecord(Object.fromEntries(keys.map((key, i) => [key, resolved[i]]))));
|
|
1757
|
+
});
|
|
1553
1758
|
}
|
|
1554
1759
|
/**
|
|
1555
1760
|
* Collect a tuple/array of {@link Result}s, **accumulating every** `Err` and
|
|
@@ -1616,7 +1821,8 @@ function validateAll(results, merge) {
|
|
|
1616
1821
|
* per key: `{ a: Result<A, E1>; b: Result<B, E2> }` yields
|
|
1617
1822
|
* `["a", E1] | ["b", E2]`, so a `switch` on the key narrows the error and an
|
|
1618
1823
|
* impossible pairing does not typecheck. That is what keeps two checks sharing
|
|
1619
|
-
* one error type distinguishable. Entries come in `Object.keys`
|
|
1824
|
+
* one error type distinguishable. Entries come in key order — `Object.keys`
|
|
1825
|
+
* order, then enumerable symbol keys (a symbol key is folded like any other).
|
|
1620
1826
|
*
|
|
1621
1827
|
* Every other rule matches {@link validateAll}: any `Defect` dominates and
|
|
1622
1828
|
* discards the accumulated errors, a throw in `merge` becomes a `Defect`, and
|
|
@@ -1675,7 +1881,7 @@ function validateAllFromDict(results, merge) {
|
|
|
1675
1881
|
* ```
|
|
1676
1882
|
*/
|
|
1677
1883
|
function validateAllAsync(results, merge) {
|
|
1678
|
-
return
|
|
1884
|
+
return settleOrDefect(() => settleAll(results).then((resolved) => foldArray(resolved, (errors) => merge(stripIndices(errors)))));
|
|
1679
1885
|
}
|
|
1680
1886
|
/**
|
|
1681
1887
|
* The asynchronous counterpart of {@link validateAllFromDict}: collect a record
|
|
@@ -1704,8 +1910,10 @@ function validateAllAsync(results, merge) {
|
|
|
1704
1910
|
* ```
|
|
1705
1911
|
*/
|
|
1706
1912
|
function validateAllFromDictAsync(results, merge) {
|
|
1707
|
-
|
|
1708
|
-
|
|
1913
|
+
return settleOrDefect(() => {
|
|
1914
|
+
const [keys, values] = ownEntries(results);
|
|
1915
|
+
return settleAll(values).then((resolved) => foldRecord(Object.fromEntries(keys.map((key, i) => [key, resolved[i]])), (entries) => merge(entries)));
|
|
1916
|
+
});
|
|
1709
1917
|
}
|
|
1710
1918
|
//#endregion
|
|
1711
1919
|
//#region src/facade.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unthrown",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.11.0",
|
|
4
4
|
"description": "Explicit errors as values, with a separate defect (panic) channel",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"defect",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"./package.json": "./package.json"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@btravstack/tsconfig": "0.
|
|
50
|
-
"@types/node": "26.2
|
|
51
|
-
"@vitest/coverage-v8": "
|
|
52
|
-
"tsdown": "0.
|
|
49
|
+
"@btravstack/tsconfig": "0.4.0",
|
|
50
|
+
"@types/node": "26.6.2",
|
|
51
|
+
"@vitest/coverage-v8": "5.0.1",
|
|
52
|
+
"tsdown": "0.23.0",
|
|
53
53
|
"typescript": "7.0.2",
|
|
54
|
-
"vitest": "
|
|
54
|
+
"vitest": "5.0.1"
|
|
55
55
|
},
|
|
56
56
|
"engines": {
|
|
57
57
|
"node": ">=20"
|