unthrown 5.7.0 → 5.9.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.cjs CHANGED
@@ -1126,7 +1126,7 @@ function fromThrowable(fn, qualify) {
1126
1126
  return (...args) => {
1127
1127
  try {
1128
1128
  const value = fn(...args);
1129
- return isThenable(value) ? thenableReturnDefect(value) : Ok(value);
1129
+ return isThenable(value) ? thenableReturnDefect(value, SYNC_FN_THENABLE) : Ok(value);
1130
1130
  } catch (cause) {
1131
1131
  return qualifyToResult(cause, triage);
1132
1132
  }
@@ -1169,7 +1169,7 @@ function fromSafeThrowable(fn) {
1169
1169
  return (...args) => {
1170
1170
  try {
1171
1171
  const value = fn(...args);
1172
- return isThenable(value) ? thenableReturnDefect(value) : Ok(value);
1172
+ return isThenable(value) ? thenableReturnDefect(value, SYNC_FN_THENABLE) : Ok(value);
1173
1173
  } catch (cause) {
1174
1174
  return defectRes(cause);
1175
1175
  }
@@ -1331,30 +1331,46 @@ function qualifyToResult(cause, qualify) {
1331
1331
  }
1332
1332
  }
1333
1333
  /**
1334
- * The Defect minted when a **synchronous** boundary's `fn` returns a thenable —
1335
- * i.e. an `async` function was handed to {@link fromThrowable} /
1336
- * {@link fromSafeThrowable}.
1334
+ * The message for {@link thenableReturnDefect} at a **synchronous boundary** —
1335
+ * an `async` function handed to {@link fromThrowable} / {@link fromSafeThrowable}.
1336
+ *
1337
+ * @internal
1338
+ */
1339
+ const SYNC_FN_THENABLE = "unthrown: fromThrowable/fromSafeThrowable wrap a SYNCHRONOUS function, but `fn` returned a thenable — its rejection would escape qualification. Use fromPromise/fromSafePromise instead.";
1340
+ /**
1341
+ * The message for {@link thenableReturnDefect} in an **accumulating aggregate** —
1342
+ * an `async` `merge` handed to {@link validateAll} and friends.
1343
+ *
1344
+ * @internal
1345
+ */
1346
+ const MERGE_THENABLE = "unthrown: an accumulating aggregate's `merge` must be SYNCHRONOUS, but it returned a thenable — its rejection would escape qualification.";
1347
+ /**
1348
+ * The Defect minted where a callback that must be **synchronous** returned a
1349
+ * thenable: a `fn` handed to {@link fromThrowable} / {@link fromSafeThrowable},
1350
+ * or a `merge` handed to an accumulating aggregate.
1337
1351
  *
1338
1352
  * @remarks
1339
1353
  * This is the sibling of the thenable-`qualify` net in {@link qualifyToResult},
1340
1354
  * and it closes a strictly worse hole. A synchronous boundary only ever sees a
1341
1355
  * synchronous `throw`, so an async `fn`'s rejection never reaches `qualify` at
1342
1356
  * all: it would sit inside `Ok(<Promise>)`, un-triaged, and then float as an
1343
- * unhandled rejection — which terminates the process on Node by default.
1344
- *
1345
- * Unlike the combinator callbacks, this cannot be banned at compile time
1346
- * without collateral damage: `T & NotThenable<T>` on `fn`'s return makes a
1347
- * **generic** function unassignable, so `fromSafeThrowable(structuredClone)`
1348
- * stops compiling and `T` collapses to `unknown`. (The phantom rest-tuple guard
1349
- * `fromPromise` uses fares worse.) So the ban is enforced here, at runtime,
1350
- * where it costs nothing: a Defect, plus adopt-and-silence so the orphaned
1351
- * rejection cannot float.
1357
+ * unhandled rejection — which terminates the process on Node by default. An
1358
+ * async `merge` is the same hole one channel over: `Err(<Promise>)`.
1359
+ *
1360
+ * The `fn` case cannot be banned at compile time without collateral damage:
1361
+ * `T & NotThenable<T>` on `fn`'s return makes a **generic** function
1362
+ * unassignable, so `fromSafeThrowable(structuredClone)` stops compiling and `T`
1363
+ * collapses to `unknown`. (The phantom rest-tuple guard `fromPromise` uses fares
1364
+ * worse.) `merge` *is* `NotThenable`-constrained, but a cast or an untyped
1365
+ * 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
1367
+ * cannot float.
1352
1368
  *
1353
1369
  * @internal
1354
1370
  */
1355
- function thenableReturnDefect(value) {
1356
- Promise.resolve(value).then(void 0, () => void 0);
1357
- return defectRes(/* @__PURE__ */ new TypeError("unthrown: fromThrowable/fromSafeThrowable wrap a SYNCHRONOUS function, but `fn` returned a thenable — its rejection would escape qualification. Use fromPromise/fromSafePromise instead."));
1371
+ function thenableReturnDefect(value, message) {
1372
+ silenceIfThenable(value);
1373
+ return defectRes(new TypeError(message));
1358
1374
  }
1359
1375
  /**
1360
1376
  * Fold an array of settled `Result`s: first `Err` wins, any `Defect` dominates,
@@ -1366,11 +1382,23 @@ function thenableReturnDefect(value) {
1366
1382
  function nonResultDefect() {
1367
1383
  return defectRes(/* @__PURE__ */ new TypeError("unthrown: aggregate received a non-Result element"));
1368
1384
  }
1369
- function foldArray(results) {
1385
+ /**
1386
+ * Resolve every input concurrently (order preserved), adopting each one
1387
+ * defensively: a cast/untyped rejecting thenable becomes a `Defect` rather than
1388
+ * rejecting the internal promise, so the "an `AsyncResult`'s internal promise
1389
+ * never rejects" invariant holds even for out-of-contract input.
1390
+ *
1391
+ * @internal
1392
+ */
1393
+ function settleAll(results) {
1394
+ return Promise.all(results.map((r) => Promise.resolve(r).then((x) => x, (cause) => defectRes(cause))));
1395
+ }
1396
+ function foldArray(results, merge) {
1370
1397
  let firstErr;
1371
1398
  let firstDefect;
1372
1399
  const values = [];
1373
- for (const r of results) {
1400
+ const errors = [];
1401
+ for (const [i, r] of results.entries()) {
1374
1402
  if (!isResult(r)) {
1375
1403
  firstDefect ??= nonResultDefect();
1376
1404
  break;
@@ -1378,10 +1406,19 @@ function foldArray(results) {
1378
1406
  if (r.tag === "Defect") {
1379
1407
  firstDefect ??= r;
1380
1408
  break;
1381
- } else if (r.tag === "Err") firstErr ??= r;
1409
+ } else if (r.tag === "Err") if (merge) errors.push([i, r.error]);
1410
+ else firstErr ??= r;
1382
1411
  else values.push(r.value);
1383
1412
  }
1384
- return firstDefect ?? firstErr ?? Ok(values);
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);
1418
+ } catch (cause) {
1419
+ return defectRes(cause);
1420
+ }
1421
+ return firstErr ?? Ok(values);
1385
1422
  }
1386
1423
  /**
1387
1424
  * Fold a record of settled `Result`s with the same rules, else `Ok` of the
@@ -1399,9 +1436,17 @@ function foldArray(results) {
1399
1436
  *
1400
1437
  * @internal
1401
1438
  */
1402
- function foldRecord(results) {
1439
+ function foldRecord(results, merge) {
1403
1440
  const keys = Object.keys(results);
1404
- return foldArray(Object.values(results)).map((values) => Object.fromEntries(keys.map((key, i) => [key, values[i]])));
1441
+ return foldArray(Object.values(results), merge && ((errors) => merge(nameErrors(errors, keys)))).map((values) => Object.fromEntries(keys.map((key, i) => [key, values[i]])));
1442
+ }
1443
+ /** Drop the accumulated indices — the positional forms merge errors alone. @internal */
1444
+ function stripIndices(errors) {
1445
+ return errors.map(([, e]) => e);
1446
+ }
1447
+ /** Pair each accumulated index back onto its key. @internal */
1448
+ function nameErrors(errors, keys) {
1449
+ return errors.map(([i, e]) => [keys[i], e]);
1405
1450
  }
1406
1451
  /**
1407
1452
  * Collect a tuple/array of {@link Result}s into a single `Result` of all their
@@ -1413,7 +1458,8 @@ function foldRecord(results) {
1413
1458
  * `Err`. A **fixed tuple** keeps its positional types — `all([Ok(1), Ok("a")])`
1414
1459
  * is `Result<[number, string], …>` — while a **dynamic array** `Result<T, E>[]`
1415
1460
  * collapses to `Result<T[], E>` with no cast. For a **record** keyed by name,
1416
- * use {@link allFromDict}.
1461
+ * use {@link allFromDict}. To report **every** `Err` instead of only the first,
1462
+ * use {@link validateAll}.
1417
1463
  *
1418
1464
  * @category Aggregate
1419
1465
  *
@@ -1436,7 +1482,9 @@ function all(results) {
1436
1482
  *
1437
1483
  * @remarks
1438
1484
  * Same folding rules as {@link all}: first `Err` short-circuits, any `Defect`
1439
- * dominates. This is **not** error accumulation.
1485
+ * dominates. This is **not** error accumulation — for that, reach for
1486
+ * {@link validateAllFromDict}, which accumulates every `Err` and folds them into
1487
+ * one modeled error.
1440
1488
  *
1441
1489
  * @category Aggregate
1442
1490
  *
@@ -1459,7 +1507,8 @@ function allFromDict(results) {
1459
1507
  * The inputs are resolved **concurrently** (order preserved); the resolved
1460
1508
  * `Result`s are then folded with the same rules as {@link all} — first `Err`
1461
1509
  * short-circuits, any `Defect` dominates. As ever, the returned `AsyncResult`'s
1462
- * internal promise never rejects. For a **record**, use {@link allFromDictAsync}.
1510
+ * internal promise never rejects. For a **record**, use {@link allFromDictAsync};
1511
+ * to report **every** `Err`, use {@link validateAllAsync}.
1463
1512
  *
1464
1513
  * @category Aggregate
1465
1514
  *
@@ -1475,7 +1524,7 @@ function allFromDict(results) {
1475
1524
  * ```
1476
1525
  */
1477
1526
  function allAsync(results) {
1478
- return new AsyncRes(Promise.all(results.map((r) => Promise.resolve(r).then((x) => x, (cause) => defectRes(cause)))).then((resolved) => foldArray(resolved)));
1527
+ return new AsyncRes(settleAll(results).then((resolved) => foldArray(resolved)));
1479
1528
  }
1480
1529
  /**
1481
1530
  * The asynchronous counterpart of {@link allFromDict}: combine a record of
@@ -1483,7 +1532,8 @@ function allAsync(results) {
1483
1532
  *
1484
1533
  * @remarks
1485
1534
  * Resolved concurrently (order preserved), folded with the {@link all} rules,
1486
- * and the internal promise never rejects.
1535
+ * and the internal promise never rejects. To report **every** `Err`, use
1536
+ * {@link validateAllFromDictAsync}.
1487
1537
  *
1488
1538
  * @category Aggregate
1489
1539
  *
@@ -1500,7 +1550,163 @@ function allAsync(results) {
1500
1550
  */
1501
1551
  function allFromDictAsync(results) {
1502
1552
  const keys = Object.keys(results);
1503
- return new AsyncRes(Promise.all(Object.values(results).map((ar) => Promise.resolve(ar).then((x) => x, (cause) => defectRes(cause)))).then((resolved) => foldArray(resolved).map((values) => Object.fromEntries(keys.map((key, i) => [key, values[i]])))));
1553
+ return new AsyncRes(settleAll(Object.values(results)).then((resolved) => foldRecord(Object.fromEntries(keys.map((key, i) => [key, resolved[i]])))));
1554
+ }
1555
+ /**
1556
+ * Collect a tuple/array of {@link Result}s, **accumulating every** `Err` and
1557
+ * merging them into a single modeled error — the accumulating counterpart of
1558
+ * {@link all}.
1559
+ *
1560
+ * @remarks
1561
+ * Same success channel as {@link all}: a **fixed tuple** keeps its positional
1562
+ * types, a **dynamic array** collapses to `Result<T[], E2>`. The difference is
1563
+ * the error channel — instead of the first `Err` winning, every `Err` is
1564
+ * collected in input order and handed to `merge`, whose return becomes the
1565
+ * modeled error.
1566
+ *
1567
+ * `merge` receives a **non-empty** list, so it is total: it is called only when
1568
+ * at least one `Err` was collected. It is **not** called when every element is
1569
+ * `Ok`, nor when a `Defect` is present.
1570
+ *
1571
+ * Any `Defect` still **dominates** — it wins over the accumulated errors, which
1572
+ * are discarded and never reach `merge`. A defect means something in this batch
1573
+ * failed in a way nobody modeled, so the violations computed alongside it are
1574
+ * not trustworthy. An out-of-contract non-`Result` element becomes a
1575
+ * `TypeError`-caused `Defect` the same way, and a throw inside `merge` becomes
1576
+ * a `Defect` too.
1577
+ *
1578
+ * `merge` must be **synchronous** — an `async` one is a compile error
1579
+ * ({@link NotThenable}), since a `Promise` in `E` is an unqualified rejection.
1580
+ *
1581
+ * For **schema-shaped** input (a request body, a form), reach for
1582
+ * `@unthrown/standard-schema`'s `fromSchema` instead — a validator already
1583
+ * hands you every issue as the modeled error. `validateAll` is for independent
1584
+ * checks you wrote yourself. For a **record** keyed by name, use
1585
+ * {@link validateAllFromDict}.
1586
+ *
1587
+ * @typeParam Rs - the tuple/array of input `Result` types.
1588
+ * @typeParam E2 - the merged error type.
1589
+ * @param results - the results to collect.
1590
+ * @param merge - folds the collected errors into one modeled error.
1591
+ *
1592
+ * @category Aggregate
1593
+ *
1594
+ * @example
1595
+ * ```ts
1596
+ * import { validateAll, Ok, Err } from "unthrown";
1597
+ *
1598
+ * // every Err is collected, not just the first
1599
+ * validateAll([Ok(1), Err("stock"), Err("credit")], (errors) => errors.join(" and "));
1600
+ * // => Err("stock and credit")
1601
+ *
1602
+ * // all-Ok keeps the positional tuple; `merge` never runs
1603
+ * validateAll([Ok(1), Ok("a")], (errors) => errors.join());
1604
+ * // => Ok([1, "a"]) typed Result<[number, string], string>
1605
+ * ```
1606
+ */
1607
+ function validateAll(results, merge) {
1608
+ return foldArray(results, (errors) => merge(stripIndices(errors)));
1609
+ }
1610
+ /**
1611
+ * Collect a **record** of {@link Result}s, accumulating every `Err` — the
1612
+ * accumulating counterpart of {@link allFromDict}, and the named counterpart of
1613
+ * {@link validateAll}.
1614
+ *
1615
+ * @remarks
1616
+ * `merge` receives a non-empty list of **`[key, error]` entries**, correlated
1617
+ * per key: `{ a: Result<A, E1>; b: Result<B, E2> }` yields
1618
+ * `["a", E1] | ["b", E2]`, so a `switch` on the key narrows the error and an
1619
+ * impossible pairing does not typecheck. That is what keeps two checks sharing
1620
+ * one error type distinguishable. Entries come in `Object.keys` order.
1621
+ *
1622
+ * Every other rule matches {@link validateAll}: any `Defect` dominates and
1623
+ * discards the accumulated errors, a throw in `merge` becomes a `Defect`, and
1624
+ * `merge` must be synchronous.
1625
+ *
1626
+ * @typeParam R - the record of input `Result` types.
1627
+ * @typeParam E2 - the merged error type.
1628
+ * @param results - the results to collect, keyed by name.
1629
+ * @param merge - folds the collected `[key, error]` entries into one error.
1630
+ *
1631
+ * @category Aggregate
1632
+ *
1633
+ * @example
1634
+ * ```ts
1635
+ * import { validateAllFromDict, Ok, Err } from "unthrown";
1636
+ *
1637
+ * validateAllFromDict(
1638
+ * { vatRate: Err("out of range"), currency: Ok("EUR"), dueDate: Err("past") },
1639
+ * (entries) => entries.map(([key, error]) => `${key}: ${error}`).join("; "),
1640
+ * );
1641
+ * // => Err("vatRate: out of range; dueDate: past")
1642
+ * ```
1643
+ */
1644
+ function validateAllFromDict(results, merge) {
1645
+ return foldRecord(results, (entries) => merge(entries));
1646
+ }
1647
+ /**
1648
+ * The asynchronous counterpart of {@link validateAll}: collect a tuple/array of
1649
+ * {@link AsyncResult}s, accumulating every `Err` into one merged error.
1650
+ *
1651
+ * @remarks
1652
+ * Every {@link validateAll} rule holds, with the inputs resolved
1653
+ * **concurrently** (order preserved) — as with {@link allAsync}, no work is
1654
+ * short-circuited either way; the fail-fast/accumulating split is purely which
1655
+ * errors get reported. The internal promise never rejects: an out-of-contract
1656
+ * rejecting thenable becomes a dominating `Defect`. `merge` stays synchronous
1657
+ * here too — this is exactly where its rejection would land unqualified in `E`.
1658
+ * For a **record**, use {@link validateAllFromDictAsync}.
1659
+ *
1660
+ * @typeParam Rs - the tuple/array of input `AsyncResult` types.
1661
+ * @typeParam E2 - the merged error type.
1662
+ * @param results - the async results to collect.
1663
+ * @param merge - folds the collected errors into one modeled error.
1664
+ *
1665
+ * @category Aggregate
1666
+ *
1667
+ * @example
1668
+ * ```ts
1669
+ * import { validateAllAsync, OkAsync, ErrAsync } from "unthrown";
1670
+ *
1671
+ * const checked = validateAllAsync(
1672
+ * [OkAsync(1), ErrAsync("stock"), ErrAsync("credit")],
1673
+ * (errors) => errors.join(" and "),
1674
+ * );
1675
+ * // (await checked) => Err("stock and credit")
1676
+ * ```
1677
+ */
1678
+ function validateAllAsync(results, merge) {
1679
+ return new AsyncRes(settleAll(results).then((resolved) => foldArray(resolved, (errors) => merge(stripIndices(errors)))));
1680
+ }
1681
+ /**
1682
+ * The asynchronous counterpart of {@link validateAllFromDict}: collect a record
1683
+ * of {@link AsyncResult}s, accumulating every `Err` into one merged error.
1684
+ *
1685
+ * @remarks
1686
+ * The {@link validateAllFromDict} rules, over inputs resolved concurrently as
1687
+ * in {@link validateAllAsync}.
1688
+ *
1689
+ * @typeParam R - the record of input `AsyncResult` types.
1690
+ * @typeParam E2 - the merged error type.
1691
+ * @param results - the async results to collect, keyed by name.
1692
+ * @param merge - folds the collected `[key, error]` entries into one error.
1693
+ *
1694
+ * @category Aggregate
1695
+ *
1696
+ * @example
1697
+ * ```ts
1698
+ * import { validateAllFromDictAsync, OkAsync, ErrAsync } from "unthrown";
1699
+ *
1700
+ * const checked = validateAllFromDictAsync(
1701
+ * { stock: ErrAsync("none left"), credit: OkAsync(500) },
1702
+ * (entries) => entries.map(([key, error]) => `${key}: ${error}`).join("; "),
1703
+ * );
1704
+ * // (await checked) => Err("stock: none left")
1705
+ * ```
1706
+ */
1707
+ 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))));
1504
1710
  }
1505
1711
  //#endregion
1506
1712
  //#region src/facade.ts
@@ -1509,7 +1715,8 @@ function allFromDictAsync(results) {
1509
1715
  * single, discoverable namespace: {@link Result.Ok}, {@link Result.Err},
1510
1716
  * {@link Result.Do}, {@link Result.fromNullable}, {@link Result.fromThrowable},
1511
1717
  * {@link Result.fromSafeThrowable}, {@link Result.all},
1512
- * {@link Result.allFromDict}, {@link Result.isOk}, {@link Result.isErr},
1718
+ * {@link Result.allFromDict}, {@link Result.validateAll},
1719
+ * {@link Result.validateAllFromDict}, {@link Result.isOk}, {@link Result.isErr},
1513
1720
  * {@link Result.isDefect}, {@link Result.isResult}.
1514
1721
  *
1515
1722
  * @remarks
@@ -1539,6 +1746,8 @@ const Result = {
1539
1746
  fromSafeThrowable,
1540
1747
  all,
1541
1748
  allFromDict,
1749
+ validateAll,
1750
+ validateAllFromDict,
1542
1751
  isOk,
1543
1752
  isErr,
1544
1753
  isDefect,
@@ -1549,7 +1758,8 @@ const Result = {
1549
1758
  * the matching namespace: {@link AsyncResult.Ok}, {@link AsyncResult.Err},
1550
1759
  * {@link AsyncResult.Do}, {@link AsyncResult.fromExecutor},
1551
1760
  * {@link AsyncResult.fromPromise}, {@link AsyncResult.fromSafePromise},
1552
- * {@link AsyncResult.all}, {@link AsyncResult.allFromDict}.
1761
+ * {@link AsyncResult.all}, {@link AsyncResult.allFromDict},
1762
+ * {@link AsyncResult.validateAll}, {@link AsyncResult.validateAllFromDict}.
1553
1763
  *
1554
1764
  * @remarks
1555
1765
  * The async sibling of {@link Result}. Statics are grouped by what they
@@ -1560,7 +1770,8 @@ const Result = {
1560
1770
  * functions carry (`AsyncResult.Ok` is `OkAsync`; `AsyncResult.Err` is
1561
1771
  * `ErrAsync`; `AsyncResult.Do` is `DoAsync`; `AsyncResult.all` is `allAsync`;
1562
1772
  * `AsyncResult.allFromDict` is
1563
- * `allFromDictAsync`). Like {@link Result}, the free functions remain the
1773
+ * `allFromDictAsync`; `AsyncResult.validateAll` is `validateAllAsync`). Like
1774
+ * {@link Result}, the free functions remain the
1564
1775
  * primary, tree-shakeable API; the value `AsyncResult` and the type
1565
1776
  * {@link AsyncResult} share one name.
1566
1777
  *
@@ -1584,7 +1795,9 @@ const AsyncResult = {
1584
1795
  fromPromise,
1585
1796
  fromSafePromise,
1586
1797
  all: allAsync,
1587
- allFromDict: allFromDictAsync
1798
+ allFromDict: allFromDictAsync,
1799
+ validateAll: validateAllAsync,
1800
+ validateAllFromDict: validateAllFromDictAsync
1588
1801
  };
1589
1802
  //#endregion
1590
1803
  //#region src/tagged.ts
@@ -1704,3 +1917,7 @@ exports.isErr = isErr;
1704
1917
  exports.isOk = isOk;
1705
1918
  exports.isResult = isResult;
1706
1919
  exports.match = match;
1920
+ exports.validateAll = validateAll;
1921
+ exports.validateAllAsync = validateAllAsync;
1922
+ exports.validateAllFromDict = validateAllFromDict;
1923
+ exports.validateAllFromDictAsync = validateAllFromDictAsync;