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/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
- let printed;
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. An `AsyncResult` is not a `Result` and returns `false`.
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
- * - through {@link silenceIfThenable}, for a value being **discarded**, where
580
- * there is no Defect channel to route to and the only correct answer is to
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
- * Adopt-and-silence a thenable a combinator is about to **discard**.
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 thenable that slipped past `NotThenable` (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
- * Adopting it costs one microtask and makes the rejection a no-op. The
605
- * boundaries already do exactly this for a thenable `qualify` and a thenable
606
- * `fn` (see `interop.ts`); this is the same net on the combinator side.
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 (isThenable(value)) Promise.resolve(value).then(void 0, () => void 0);
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 || Array.isArray(value)) throw new TypeError("bind/let requires an object scope — start a do-chain with Do()");
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
- const value = fn(...args);
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 (isThenable(returned)) Promise.resolve(returned).then(void 0, (cause) => settle(defect(cause)));
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
- Promise.resolve(q).then(void 0, () => void 0);
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 adopt-and-silence so the orphaned rejection
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
- let firstErr;
1397
- let firstDefect;
1398
- const values = [];
1399
- const errors = [];
1400
- for (const [i, r] of results.entries()) {
1401
- if (!isResult(r)) {
1402
- firstDefect ??= nonResultDefect();
1403
- break;
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 (r.tag === "Defect") {
1406
- firstDefect ??= r;
1407
- break;
1408
- } else if (r.tag === "Err") if (merge) errors.push([i, r.error]);
1409
- else firstErr ??= r;
1410
- else values.push(r.value);
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
- const keys = Object.keys(results);
1440
- return foldArray(Object.values(results), merge && ((errors) => merge(nameErrors(errors, keys)))).map((values) => Object.fromEntries(keys.map((key, i) => [key, values[i]])));
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 new AsyncRes(settleAll(results).then((resolved) => foldArray(resolved)));
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
- const keys = Object.keys(results);
1552
- return new AsyncRes(settleAll(Object.values(results)).then((resolved) => foldRecord(Object.fromEntries(keys.map((key, i) => [key, resolved[i]])))));
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` order.
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 new AsyncRes(settleAll(results).then((resolved) => foldArray(resolved, (errors) => merge(stripIndices(errors)))));
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
- const keys = Object.keys(results);
1708
- return new AsyncRes(settleAll(Object.values(results)).then((resolved) => foldRecord(Object.fromEntries(keys.map((key, i) => [key, resolved[i]])), (entries) => merge(entries))));
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.9.0",
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.2.0",
50
- "@types/node": "26.2.0",
51
- "@vitest/coverage-v8": "4.1.10",
52
- "tsdown": "0.22.14",
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": "4.1.10"
54
+ "vitest": "5.0.1"
55
55
  },
56
56
  "engines": {
57
57
  "node": ">=20"