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