unthrown 5.3.0 → 5.5.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
@@ -135,7 +135,7 @@ const universal = pattern(() => true);
135
135
  /**
136
136
  * The pattern namespace (unthrown's own; the former ts-pattern `P`):
137
137
  *
138
- * - `P._` / `P.any` — the universal catch-all, and an **escape hatch** rather
138
+ * - `P._` — the universal catch-all, and an **escape hatch** rather
139
139
  * than the default: matching the error channel means naming its cases, so
140
140
  * reach for this only where they cannot be named. Matches anything, and
141
141
  * (because its phantom type is `unknown`) makes the builder provably
@@ -153,25 +153,22 @@ const universal = pattern(() => true);
153
153
  * branch's parameter to that variant, payload included. The workhorse of the
154
154
  * error channel: `matcher.with(P.tag("NotFound"), (e) => …)`. It composes like
155
155
  * any other pattern — in a grouped arm
156
- * (`.with(P.tag("A"), P.tag("B"), handler)`) and inside `P.union`.
156
+ * (`.with(P.tag("A"), P.tag("B"), handler)`).
157
157
  * - `P.instanceOf(Cls)` — an `instanceof` check, narrowing to the class
158
158
  * instance type (for union members that are not tagged, e.g. a third-party
159
159
  * error class).
160
- * - `P.when(guard)` — an arbitrary type-guard predicate.
161
- * - `P.union(…patterns)` — matches when any sub-pattern matches.
162
- * - `P.string` / `P.number` — primitive-type wildcards.
160
+ * - `P.when(guard)` — an arbitrary type-guard predicate. Also the way to match
161
+ * a primitive shape (`P.when((v): v is string => typeof v === "string")`),
162
+ * and grouping patterns under one handler is what a `.with(a, b, handler)`
163
+ * arm already does.
163
164
  *
164
165
  * @category Constructors
165
166
  */
166
167
  const P = Object.freeze({
167
168
  _: universal,
168
- any: universal,
169
169
  tag: (value) => ({ _tag: value }),
170
170
  instanceOf: (cls) => pattern((value) => value instanceof cls),
171
- when: (guard) => pattern(guard),
172
- union: (...patterns) => pattern((value) => patterns.some((sub) => matches(sub, value))),
173
- string: pattern((value) => typeof value === "string"),
174
- number: pattern((value) => typeof value === "number")
171
+ when: (guard) => pattern(guard)
175
172
  });
176
173
  //#endregion
177
174
  //#region src/defect.ts
@@ -569,6 +566,30 @@ function passThrough(self) {
569
566
  return self;
570
567
  }
571
568
  /**
569
+ * Runtime thenable probe, shared by the combinator-side net below and the
570
+ * boundary nets in `interop.ts`.
571
+ *
572
+ * @remarks
573
+ * Reading `.then` can itself **throw** — a hostile getter, or a Proxy `get`
574
+ * trap — so this is deliberately not a total function, and every caller must
575
+ * account for that. Two shapes are sanctioned, and there is no third:
576
+ *
577
+ * - inside a `try` that routes the throw to a `Defect` (the boundaries in
578
+ * `interop.ts`, where a hostile value arriving at a triage point *is* an
579
+ * 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.
583
+ *
584
+ * Calling it bare, outside both, is a bug: the throw escapes into whatever
585
+ * context invoked it.
586
+ *
587
+ * @internal
588
+ */
589
+ function isThenable(x) {
590
+ return (typeof x === "object" || typeof x === "function") && x !== null && typeof x.then === "function";
591
+ }
592
+ /**
572
593
  * Adopt-and-silence a thenable a combinator is about to **discard**.
573
594
  *
574
595
  * @remarks
@@ -589,7 +610,7 @@ function passThrough(self) {
589
610
  */
590
611
  function silenceIfThenable(value) {
591
612
  try {
592
- if ((typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function") Promise.resolve(value).then(void 0, () => void 0);
613
+ if (isThenable(value)) Promise.resolve(value).then(void 0, () => void 0);
593
614
  } catch {}
594
615
  }
595
616
  /**
@@ -670,18 +691,29 @@ var AsyncRes = class AsyncRes {
670
691
  constructor(promise) {
671
692
  this.#promise = promise;
672
693
  }
694
+ /**
695
+ * Lift a **synchronous** `Result` combinator over the wrapped promise.
696
+ *
697
+ * @remarks
698
+ * Every method below that does no `await` of its own is exactly its `Res`
699
+ * twin applied to the settled `Result` — same tag check, same try/catch, same
700
+ * throw→defect net. Delegating instead of restating it keeps the two surfaces
701
+ * from drifting: a fix to the sync combinator is the fix to the async one.
702
+ * The methods that DO await a callback's result (`flatMap`, `flatTap`,
703
+ * `bind`, `flatMapErrCases`, `flatTapErrCases`, `recoverDefect` — the ones
704
+ * whose callback may hand back an `AsyncResult`) genuinely differ and are
705
+ * still written out in full.
706
+ *
707
+ * @internal
708
+ */
709
+ #lift(f) {
710
+ return new AsyncRes(this.#promise.then(f));
711
+ }
673
712
  then(onfulfilled, onrejected) {
674
713
  return this.#promise.then(onfulfilled, onrejected);
675
714
  }
676
715
  map(f) {
677
- return new AsyncRes(this.#promise.then((r) => {
678
- if (r.tag !== "Ok") return passThrough(r);
679
- try {
680
- return okRes(f(r.value));
681
- } catch (cause) {
682
- return defectRes(cause);
683
- }
684
- }));
716
+ return this.#lift((r) => r.map(f));
685
717
  }
686
718
  flatMap(f) {
687
719
  return new AsyncRes(this.#promise.then(async (r) => {
@@ -695,15 +727,7 @@ var AsyncRes = class AsyncRes {
695
727
  }));
696
728
  }
697
729
  tap(f) {
698
- return new AsyncRes(this.#promise.then((r) => {
699
- if (r.tag !== "Ok") return r;
700
- try {
701
- silenceIfThenable(f(r.value));
702
- return r;
703
- } catch (cause) {
704
- return defectRes(cause);
705
- }
706
- }));
730
+ return this.#lift((r) => r.tap(f));
707
731
  }
708
732
  flatTap(f) {
709
733
  return new AsyncRes(this.#promise.then(async (r) => {
@@ -734,45 +758,19 @@ var AsyncRes = class AsyncRes {
734
758
  }));
735
759
  }
736
760
  let(name, f) {
737
- return new AsyncRes(this.#promise.then((r) => {
738
- if (r.tag !== "Ok") return passThrough(r);
739
- try {
740
- return okRes({
741
- ...scopeOf(r.value),
742
- [name]: f(r.value)
743
- });
744
- } catch (cause) {
745
- return defectRes(cause);
746
- }
747
- }));
761
+ return this.#lift((r) => r.let(name, f));
748
762
  }
749
763
  as(value) {
750
- return new AsyncRes(this.#promise.then((r) => r.tag === "Ok" ? okRes(value) : passThrough(r)));
764
+ return this.#lift((r) => r.as(value));
751
765
  }
752
766
  discard() {
753
- return new AsyncRes(this.#promise.then((r) => r.tag === "Ok" ? okRes(void 0) : passThrough(r)));
767
+ return this.#lift((r) => r.discard());
754
768
  }
755
769
  ensure(predicate, onFail) {
756
- return new AsyncRes(this.#promise.then((r) => {
757
- if (r.tag !== "Ok") return passThrough(r);
758
- try {
759
- return predicate(r.value) ? r : errRes(onFail(r.value));
760
- } catch (cause) {
761
- return defectRes(cause);
762
- }
763
- }));
770
+ return this.#lift((r) => r.ensure(predicate, onFail));
764
771
  }
765
772
  mapErrCases(f) {
766
- return new AsyncRes(this.#promise.then((r) => {
767
- if (r.tag !== "Err") return passThrough(r);
768
- try {
769
- const out = runMatch(f, r.error);
770
- if (isDefectMarker(out)) return defectRes(out.cause);
771
- return errRes(out);
772
- } catch (cause) {
773
- return defectRes(cause);
774
- }
775
- }));
773
+ return this.#lift((r) => r.mapErrCases(f));
776
774
  }
777
775
  flatMapErrCases(f) {
778
776
  return new AsyncRes(this.#promise.then(async (r) => {
@@ -789,29 +787,10 @@ var AsyncRes = class AsyncRes {
789
787
  }));
790
788
  }
791
789
  recoverErrCases(f) {
792
- return new AsyncRes(this.#promise.then((r) => {
793
- if (r.tag !== "Err") return passThrough(r);
794
- try {
795
- const out = runMatch(f, r.error);
796
- if (isDefectMarker(out)) return defectRes(out.cause);
797
- return okRes(out);
798
- } catch (cause) {
799
- return defectRes(cause);
800
- }
801
- }));
790
+ return this.#lift((r) => r.recoverErrCases(f));
802
791
  }
803
792
  tapErrCases(f) {
804
- return new AsyncRes(this.#promise.then((r) => {
805
- if (r.tag !== "Err") return r;
806
- try {
807
- const out = runMatch(f, r.error);
808
- if (isDefectMarker(out)) return observerThrowToDefect(out.cause, r.error);
809
- silenceIfThenable(out);
810
- return r;
811
- } catch (cause) {
812
- return observerThrowToDefect(cause, r.error);
813
- }
814
- }));
793
+ return this.#lift((r) => r.tapErrCases(f));
815
794
  }
816
795
  flatTapErrCases(f) {
817
796
  return new AsyncRes(this.#promise.then(async (r) => {
@@ -839,26 +818,10 @@ var AsyncRes = class AsyncRes {
839
818
  }));
840
819
  }
841
820
  tapDefect(f) {
842
- return new AsyncRes(this.#promise.then((r) => {
843
- if (r.tag !== "Defect") return r;
844
- try {
845
- silenceIfThenable(f(r.cause));
846
- return r;
847
- } catch (cause) {
848
- return observerThrowToDefect(cause, r.cause);
849
- }
850
- }));
821
+ return this.#lift((r) => r.tapDefect(f));
851
822
  }
852
823
  tapFailure(f) {
853
- return new AsyncRes(this.#promise.then((r) => {
854
- if (r.tag === "Ok") return r;
855
- try {
856
- silenceIfThenable(f(r));
857
- return r;
858
- } catch (cause) {
859
- return observerThrowToDefect(cause, r.tag === "Err" ? r.error : r.cause);
860
- }
861
- }));
824
+ return this.#lift((r) => r.tapFailure(f));
862
825
  }
863
826
  match(cases) {
864
827
  return this.#promise.then((r) => r.match(cases));
@@ -1340,7 +1303,7 @@ function fromExecutor(executor) {
1340
1303
  return;
1341
1304
  }
1342
1305
  if (!isResult(result)) {
1343
- if (isThenable(result)) Promise.resolve(result).then(void 0, () => void 0);
1306
+ silenceIfThenable(result);
1344
1307
  resolve(defectRes(/* @__PURE__ */ new TypeError("unthrown: fromExecutor's settle received a non-Result value")));
1345
1308
  return;
1346
1309
  }
@@ -1394,15 +1357,6 @@ function thenableReturnDefect(value) {
1394
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."));
1395
1358
  }
1396
1359
  /**
1397
- * Runtime thenable probe for the belt-and-braces guards above. Called inside the
1398
- * caller's `try`, so even a hostile `.then` getter lands on the Defect path.
1399
- *
1400
- * @internal
1401
- */
1402
- function isThenable(x) {
1403
- return (typeof x === "object" || typeof x === "function") && x !== null && typeof x.then === "function";
1404
- }
1405
- /**
1406
1360
  * Fold an array of settled `Result`s: first `Err` wins, any `Defect` dominates,
1407
1361
  * else `Ok` of the values array.
1408
1362
  *
@@ -1431,32 +1385,23 @@ function foldArray(results) {
1431
1385
  }
1432
1386
  /**
1433
1387
  * Fold a record of settled `Result`s with the same rules, else `Ok` of the
1434
- * record of values. Keys are written with `Object.defineProperty` so a
1435
- * caller-supplied `"__proto__"` key cannot pollute the prototype.
1388
+ * record of values.
1389
+ *
1390
+ * @remarks
1391
+ * The positional fold already implements every rule (first `Err` wins, any
1392
+ * `Defect` dominates, a non-`Result` element becomes a `Defect`), so this pairs
1393
+ * the keys back onto its success value rather than restating them.
1394
+ *
1395
+ * `Object.fromEntries` is what makes a caller-supplied `"__proto__"` key safe:
1396
+ * it builds each key with CreateDataProperty, which defines an own property
1397
+ * instead of invoking the `__proto__` setter — the same guarantee the previous
1398
+ * explicit `Object.defineProperty` loop bought by hand.
1436
1399
  *
1437
1400
  * @internal
1438
1401
  */
1439
1402
  function foldRecord(results) {
1440
- let firstErr;
1441
- let firstDefect;
1442
- const values = {};
1443
- for (const [key, r] of Object.entries(results)) {
1444
- if (!isResult(r)) {
1445
- firstDefect ??= nonResultDefect();
1446
- break;
1447
- }
1448
- if (r.tag === "Defect") {
1449
- firstDefect ??= r;
1450
- break;
1451
- } else if (r.tag === "Err") firstErr ??= r;
1452
- else Object.defineProperty(values, key, {
1453
- value: r.value,
1454
- enumerable: true,
1455
- writable: true,
1456
- configurable: true
1457
- });
1458
- }
1459
- return firstDefect ?? firstErr ?? Ok(values);
1403
+ const keys = Object.keys(results);
1404
+ return foldArray(Object.values(results)).map((values) => Object.fromEntries(keys.map((key, i) => [key, values[i]])));
1460
1405
  }
1461
1406
  /**
1462
1407
  * Collect a tuple/array of {@link Result}s into a single `Result` of all their
@@ -1554,14 +1499,8 @@ function allAsync(results) {
1554
1499
  * ```
1555
1500
  */
1556
1501
  function allFromDictAsync(results) {
1557
- const entries = Object.entries(results);
1558
- return new AsyncRes(Promise.all(entries.map(([, ar]) => Promise.resolve(ar).then((x) => x, (cause) => defectRes(cause)))).then((resolved) => {
1559
- const byKey = Object.create(null);
1560
- entries.forEach(([key], i) => {
1561
- byKey[key] = resolved[i];
1562
- });
1563
- return foldRecord(byKey);
1564
- }));
1502
+ 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]])))));
1565
1504
  }
1566
1505
  //#endregion
1567
1506
  //#region src/facade.ts
package/dist/index.d.cts CHANGED
@@ -44,7 +44,7 @@ type PatternMatcher<M> = {
44
44
  readonly [MATCHES]?: M;
45
45
  };
46
46
  /**
47
- * The statically-known universal pattern — the type of `P._` / `P.any` only.
47
+ * The statically-known universal pattern — the type of `P._` only.
48
48
  * The phantom `UNIVERSAL` marker is *required*, so no other
49
49
  * `PatternMatcher<unknown>` (e.g. a `P.when` guard that happens to be
50
50
  * universal) is assignable: the catch-all `.with` overload must only fire for
@@ -127,7 +127,7 @@ type PinTooLate = {
127
127
  */
128
128
  type Matcher<E, Remaining, O, Declared = Unset> = {
129
129
  /**
130
- * The catch-all arm: `.with(P._, handler)` / `.with(P.any, handler)` — the
130
+ * The catch-all arm: `.with(P._, handler)` — the
131
131
  * wildcard **escape hatch**, not the way to handle a concrete error union
132
132
  * (name those cases; `@unthrown/oxlint`'s `no-catch-all-pattern`, in its
133
133
  * `recommended` preset, flags the wildcard).
@@ -225,7 +225,7 @@ declare function match<const E>(value: E): Matcher<E, E, never>;
225
225
  /**
226
226
  * The pattern namespace (unthrown's own; the former ts-pattern `P`):
227
227
  *
228
- * - `P._` / `P.any` — the universal catch-all, and an **escape hatch** rather
228
+ * - `P._` — the universal catch-all, and an **escape hatch** rather
229
229
  * than the default: matching the error channel means naming its cases, so
230
230
  * reach for this only where they cannot be named. Matches anything, and
231
231
  * (because its phantom type is `unknown`) makes the builder provably
@@ -243,27 +243,24 @@ declare function match<const E>(value: E): Matcher<E, E, never>;
243
243
  * branch's parameter to that variant, payload included. The workhorse of the
244
244
  * error channel: `matcher.with(P.tag("NotFound"), (e) => …)`. It composes like
245
245
  * any other pattern — in a grouped arm
246
- * (`.with(P.tag("A"), P.tag("B"), handler)`) and inside `P.union`.
246
+ * (`.with(P.tag("A"), P.tag("B"), handler)`).
247
247
  * - `P.instanceOf(Cls)` — an `instanceof` check, narrowing to the class
248
248
  * instance type (for union members that are not tagged, e.g. a third-party
249
249
  * error class).
250
- * - `P.when(guard)` — an arbitrary type-guard predicate.
251
- * - `P.union(…patterns)` — matches when any sub-pattern matches.
252
- * - `P.string` / `P.number` — primitive-type wildcards.
250
+ * - `P.when(guard)` — an arbitrary type-guard predicate. Also the way to match
251
+ * a primitive shape (`P.when((v): v is string => typeof v === "string")`),
252
+ * and grouping patterns under one handler is what a `.with(a, b, handler)`
253
+ * arm already does.
253
254
  *
254
255
  * @category Constructors
255
256
  */
256
257
  declare const P: Readonly<{
257
258
  _: UniversalPattern;
258
- any: UniversalPattern;
259
259
  tag: <const Tag extends string>(value: Tag) => {
260
260
  _tag: Tag;
261
261
  };
262
262
  instanceOf: <C extends abstract new (...args: never[]) => unknown>(cls: C) => PatternMatcher<InstanceType<C>>;
263
263
  when: <G>(guard: (value: unknown) => value is G) => PatternMatcher<G>;
264
- union: <const Pts extends readonly [unknown, ...unknown[]]>(...patterns: Pts) => PatternMatcher<MatchedOf<Pts[number]>>;
265
- string: PatternMatcher<string>;
266
- number: PatternMatcher<number>;
267
264
  }>;
268
265
  //#endregion
269
266
  //#region src/types.d.ts
package/dist/index.d.mts CHANGED
@@ -44,7 +44,7 @@ type PatternMatcher<M> = {
44
44
  readonly [MATCHES]?: M;
45
45
  };
46
46
  /**
47
- * The statically-known universal pattern — the type of `P._` / `P.any` only.
47
+ * The statically-known universal pattern — the type of `P._` only.
48
48
  * The phantom `UNIVERSAL` marker is *required*, so no other
49
49
  * `PatternMatcher<unknown>` (e.g. a `P.when` guard that happens to be
50
50
  * universal) is assignable: the catch-all `.with` overload must only fire for
@@ -127,7 +127,7 @@ type PinTooLate = {
127
127
  */
128
128
  type Matcher<E, Remaining, O, Declared = Unset> = {
129
129
  /**
130
- * The catch-all arm: `.with(P._, handler)` / `.with(P.any, handler)` — the
130
+ * The catch-all arm: `.with(P._, handler)` — the
131
131
  * wildcard **escape hatch**, not the way to handle a concrete error union
132
132
  * (name those cases; `@unthrown/oxlint`'s `no-catch-all-pattern`, in its
133
133
  * `recommended` preset, flags the wildcard).
@@ -225,7 +225,7 @@ declare function match<const E>(value: E): Matcher<E, E, never>;
225
225
  /**
226
226
  * The pattern namespace (unthrown's own; the former ts-pattern `P`):
227
227
  *
228
- * - `P._` / `P.any` — the universal catch-all, and an **escape hatch** rather
228
+ * - `P._` — the universal catch-all, and an **escape hatch** rather
229
229
  * than the default: matching the error channel means naming its cases, so
230
230
  * reach for this only where they cannot be named. Matches anything, and
231
231
  * (because its phantom type is `unknown`) makes the builder provably
@@ -243,27 +243,24 @@ declare function match<const E>(value: E): Matcher<E, E, never>;
243
243
  * branch's parameter to that variant, payload included. The workhorse of the
244
244
  * error channel: `matcher.with(P.tag("NotFound"), (e) => …)`. It composes like
245
245
  * any other pattern — in a grouped arm
246
- * (`.with(P.tag("A"), P.tag("B"), handler)`) and inside `P.union`.
246
+ * (`.with(P.tag("A"), P.tag("B"), handler)`).
247
247
  * - `P.instanceOf(Cls)` — an `instanceof` check, narrowing to the class
248
248
  * instance type (for union members that are not tagged, e.g. a third-party
249
249
  * error class).
250
- * - `P.when(guard)` — an arbitrary type-guard predicate.
251
- * - `P.union(…patterns)` — matches when any sub-pattern matches.
252
- * - `P.string` / `P.number` — primitive-type wildcards.
250
+ * - `P.when(guard)` — an arbitrary type-guard predicate. Also the way to match
251
+ * a primitive shape (`P.when((v): v is string => typeof v === "string")`),
252
+ * and grouping patterns under one handler is what a `.with(a, b, handler)`
253
+ * arm already does.
253
254
  *
254
255
  * @category Constructors
255
256
  */
256
257
  declare const P: Readonly<{
257
258
  _: UniversalPattern;
258
- any: UniversalPattern;
259
259
  tag: <const Tag extends string>(value: Tag) => {
260
260
  _tag: Tag;
261
261
  };
262
262
  instanceOf: <C extends abstract new (...args: never[]) => unknown>(cls: C) => PatternMatcher<InstanceType<C>>;
263
263
  when: <G>(guard: (value: unknown) => value is G) => PatternMatcher<G>;
264
- union: <const Pts extends readonly [unknown, ...unknown[]]>(...patterns: Pts) => PatternMatcher<MatchedOf<Pts[number]>>;
265
- string: PatternMatcher<string>;
266
- number: PatternMatcher<number>;
267
264
  }>;
268
265
  //#endregion
269
266
  //#region src/types.d.ts
package/dist/index.mjs CHANGED
@@ -134,7 +134,7 @@ const universal = pattern(() => true);
134
134
  /**
135
135
  * The pattern namespace (unthrown's own; the former ts-pattern `P`):
136
136
  *
137
- * - `P._` / `P.any` — the universal catch-all, and an **escape hatch** rather
137
+ * - `P._` — the universal catch-all, and an **escape hatch** rather
138
138
  * than the default: matching the error channel means naming its cases, so
139
139
  * reach for this only where they cannot be named. Matches anything, and
140
140
  * (because its phantom type is `unknown`) makes the builder provably
@@ -152,25 +152,22 @@ const universal = pattern(() => true);
152
152
  * branch's parameter to that variant, payload included. The workhorse of the
153
153
  * error channel: `matcher.with(P.tag("NotFound"), (e) => …)`. It composes like
154
154
  * any other pattern — in a grouped arm
155
- * (`.with(P.tag("A"), P.tag("B"), handler)`) and inside `P.union`.
155
+ * (`.with(P.tag("A"), P.tag("B"), handler)`).
156
156
  * - `P.instanceOf(Cls)` — an `instanceof` check, narrowing to the class
157
157
  * instance type (for union members that are not tagged, e.g. a third-party
158
158
  * error class).
159
- * - `P.when(guard)` — an arbitrary type-guard predicate.
160
- * - `P.union(…patterns)` — matches when any sub-pattern matches.
161
- * - `P.string` / `P.number` — primitive-type wildcards.
159
+ * - `P.when(guard)` — an arbitrary type-guard predicate. Also the way to match
160
+ * a primitive shape (`P.when((v): v is string => typeof v === "string")`),
161
+ * and grouping patterns under one handler is what a `.with(a, b, handler)`
162
+ * arm already does.
162
163
  *
163
164
  * @category Constructors
164
165
  */
165
166
  const P = Object.freeze({
166
167
  _: universal,
167
- any: universal,
168
168
  tag: (value) => ({ _tag: value }),
169
169
  instanceOf: (cls) => pattern((value) => value instanceof cls),
170
- when: (guard) => pattern(guard),
171
- union: (...patterns) => pattern((value) => patterns.some((sub) => matches(sub, value))),
172
- string: pattern((value) => typeof value === "string"),
173
- number: pattern((value) => typeof value === "number")
170
+ when: (guard) => pattern(guard)
174
171
  });
175
172
  //#endregion
176
173
  //#region src/defect.ts
@@ -568,6 +565,30 @@ function passThrough(self) {
568
565
  return self;
569
566
  }
570
567
  /**
568
+ * Runtime thenable probe, shared by the combinator-side net below and the
569
+ * boundary nets in `interop.ts`.
570
+ *
571
+ * @remarks
572
+ * Reading `.then` can itself **throw** — a hostile getter, or a Proxy `get`
573
+ * trap — so this is deliberately not a total function, and every caller must
574
+ * account for that. Two shapes are sanctioned, and there is no third:
575
+ *
576
+ * - inside a `try` that routes the throw to a `Defect` (the boundaries in
577
+ * `interop.ts`, where a hostile value arriving at a triage point *is* an
578
+ * 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.
582
+ *
583
+ * Calling it bare, outside both, is a bug: the throw escapes into whatever
584
+ * context invoked it.
585
+ *
586
+ * @internal
587
+ */
588
+ function isThenable(x) {
589
+ return (typeof x === "object" || typeof x === "function") && x !== null && typeof x.then === "function";
590
+ }
591
+ /**
571
592
  * Adopt-and-silence a thenable a combinator is about to **discard**.
572
593
  *
573
594
  * @remarks
@@ -588,7 +609,7 @@ function passThrough(self) {
588
609
  */
589
610
  function silenceIfThenable(value) {
590
611
  try {
591
- if ((typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function") Promise.resolve(value).then(void 0, () => void 0);
612
+ if (isThenable(value)) Promise.resolve(value).then(void 0, () => void 0);
592
613
  } catch {}
593
614
  }
594
615
  /**
@@ -669,18 +690,29 @@ var AsyncRes = class AsyncRes {
669
690
  constructor(promise) {
670
691
  this.#promise = promise;
671
692
  }
693
+ /**
694
+ * Lift a **synchronous** `Result` combinator over the wrapped promise.
695
+ *
696
+ * @remarks
697
+ * Every method below that does no `await` of its own is exactly its `Res`
698
+ * twin applied to the settled `Result` — same tag check, same try/catch, same
699
+ * throw→defect net. Delegating instead of restating it keeps the two surfaces
700
+ * from drifting: a fix to the sync combinator is the fix to the async one.
701
+ * The methods that DO await a callback's result (`flatMap`, `flatTap`,
702
+ * `bind`, `flatMapErrCases`, `flatTapErrCases`, `recoverDefect` — the ones
703
+ * whose callback may hand back an `AsyncResult`) genuinely differ and are
704
+ * still written out in full.
705
+ *
706
+ * @internal
707
+ */
708
+ #lift(f) {
709
+ return new AsyncRes(this.#promise.then(f));
710
+ }
672
711
  then(onfulfilled, onrejected) {
673
712
  return this.#promise.then(onfulfilled, onrejected);
674
713
  }
675
714
  map(f) {
676
- return new AsyncRes(this.#promise.then((r) => {
677
- if (r.tag !== "Ok") return passThrough(r);
678
- try {
679
- return okRes(f(r.value));
680
- } catch (cause) {
681
- return defectRes(cause);
682
- }
683
- }));
715
+ return this.#lift((r) => r.map(f));
684
716
  }
685
717
  flatMap(f) {
686
718
  return new AsyncRes(this.#promise.then(async (r) => {
@@ -694,15 +726,7 @@ var AsyncRes = class AsyncRes {
694
726
  }));
695
727
  }
696
728
  tap(f) {
697
- return new AsyncRes(this.#promise.then((r) => {
698
- if (r.tag !== "Ok") return r;
699
- try {
700
- silenceIfThenable(f(r.value));
701
- return r;
702
- } catch (cause) {
703
- return defectRes(cause);
704
- }
705
- }));
729
+ return this.#lift((r) => r.tap(f));
706
730
  }
707
731
  flatTap(f) {
708
732
  return new AsyncRes(this.#promise.then(async (r) => {
@@ -733,45 +757,19 @@ var AsyncRes = class AsyncRes {
733
757
  }));
734
758
  }
735
759
  let(name, f) {
736
- return new AsyncRes(this.#promise.then((r) => {
737
- if (r.tag !== "Ok") return passThrough(r);
738
- try {
739
- return okRes({
740
- ...scopeOf(r.value),
741
- [name]: f(r.value)
742
- });
743
- } catch (cause) {
744
- return defectRes(cause);
745
- }
746
- }));
760
+ return this.#lift((r) => r.let(name, f));
747
761
  }
748
762
  as(value) {
749
- return new AsyncRes(this.#promise.then((r) => r.tag === "Ok" ? okRes(value) : passThrough(r)));
763
+ return this.#lift((r) => r.as(value));
750
764
  }
751
765
  discard() {
752
- return new AsyncRes(this.#promise.then((r) => r.tag === "Ok" ? okRes(void 0) : passThrough(r)));
766
+ return this.#lift((r) => r.discard());
753
767
  }
754
768
  ensure(predicate, onFail) {
755
- return new AsyncRes(this.#promise.then((r) => {
756
- if (r.tag !== "Ok") return passThrough(r);
757
- try {
758
- return predicate(r.value) ? r : errRes(onFail(r.value));
759
- } catch (cause) {
760
- return defectRes(cause);
761
- }
762
- }));
769
+ return this.#lift((r) => r.ensure(predicate, onFail));
763
770
  }
764
771
  mapErrCases(f) {
765
- return new AsyncRes(this.#promise.then((r) => {
766
- if (r.tag !== "Err") return passThrough(r);
767
- try {
768
- const out = runMatch(f, r.error);
769
- if (isDefectMarker(out)) return defectRes(out.cause);
770
- return errRes(out);
771
- } catch (cause) {
772
- return defectRes(cause);
773
- }
774
- }));
772
+ return this.#lift((r) => r.mapErrCases(f));
775
773
  }
776
774
  flatMapErrCases(f) {
777
775
  return new AsyncRes(this.#promise.then(async (r) => {
@@ -788,29 +786,10 @@ var AsyncRes = class AsyncRes {
788
786
  }));
789
787
  }
790
788
  recoverErrCases(f) {
791
- return new AsyncRes(this.#promise.then((r) => {
792
- if (r.tag !== "Err") return passThrough(r);
793
- try {
794
- const out = runMatch(f, r.error);
795
- if (isDefectMarker(out)) return defectRes(out.cause);
796
- return okRes(out);
797
- } catch (cause) {
798
- return defectRes(cause);
799
- }
800
- }));
789
+ return this.#lift((r) => r.recoverErrCases(f));
801
790
  }
802
791
  tapErrCases(f) {
803
- return new AsyncRes(this.#promise.then((r) => {
804
- if (r.tag !== "Err") return r;
805
- try {
806
- const out = runMatch(f, r.error);
807
- if (isDefectMarker(out)) return observerThrowToDefect(out.cause, r.error);
808
- silenceIfThenable(out);
809
- return r;
810
- } catch (cause) {
811
- return observerThrowToDefect(cause, r.error);
812
- }
813
- }));
792
+ return this.#lift((r) => r.tapErrCases(f));
814
793
  }
815
794
  flatTapErrCases(f) {
816
795
  return new AsyncRes(this.#promise.then(async (r) => {
@@ -838,26 +817,10 @@ var AsyncRes = class AsyncRes {
838
817
  }));
839
818
  }
840
819
  tapDefect(f) {
841
- return new AsyncRes(this.#promise.then((r) => {
842
- if (r.tag !== "Defect") return r;
843
- try {
844
- silenceIfThenable(f(r.cause));
845
- return r;
846
- } catch (cause) {
847
- return observerThrowToDefect(cause, r.cause);
848
- }
849
- }));
820
+ return this.#lift((r) => r.tapDefect(f));
850
821
  }
851
822
  tapFailure(f) {
852
- return new AsyncRes(this.#promise.then((r) => {
853
- if (r.tag === "Ok") return r;
854
- try {
855
- silenceIfThenable(f(r));
856
- return r;
857
- } catch (cause) {
858
- return observerThrowToDefect(cause, r.tag === "Err" ? r.error : r.cause);
859
- }
860
- }));
823
+ return this.#lift((r) => r.tapFailure(f));
861
824
  }
862
825
  match(cases) {
863
826
  return this.#promise.then((r) => r.match(cases));
@@ -1339,7 +1302,7 @@ function fromExecutor(executor) {
1339
1302
  return;
1340
1303
  }
1341
1304
  if (!isResult(result)) {
1342
- if (isThenable(result)) Promise.resolve(result).then(void 0, () => void 0);
1305
+ silenceIfThenable(result);
1343
1306
  resolve(defectRes(/* @__PURE__ */ new TypeError("unthrown: fromExecutor's settle received a non-Result value")));
1344
1307
  return;
1345
1308
  }
@@ -1393,15 +1356,6 @@ function thenableReturnDefect(value) {
1393
1356
  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."));
1394
1357
  }
1395
1358
  /**
1396
- * Runtime thenable probe for the belt-and-braces guards above. Called inside the
1397
- * caller's `try`, so even a hostile `.then` getter lands on the Defect path.
1398
- *
1399
- * @internal
1400
- */
1401
- function isThenable(x) {
1402
- return (typeof x === "object" || typeof x === "function") && x !== null && typeof x.then === "function";
1403
- }
1404
- /**
1405
1359
  * Fold an array of settled `Result`s: first `Err` wins, any `Defect` dominates,
1406
1360
  * else `Ok` of the values array.
1407
1361
  *
@@ -1430,32 +1384,23 @@ function foldArray(results) {
1430
1384
  }
1431
1385
  /**
1432
1386
  * Fold a record of settled `Result`s with the same rules, else `Ok` of the
1433
- * record of values. Keys are written with `Object.defineProperty` so a
1434
- * caller-supplied `"__proto__"` key cannot pollute the prototype.
1387
+ * record of values.
1388
+ *
1389
+ * @remarks
1390
+ * The positional fold already implements every rule (first `Err` wins, any
1391
+ * `Defect` dominates, a non-`Result` element becomes a `Defect`), so this pairs
1392
+ * the keys back onto its success value rather than restating them.
1393
+ *
1394
+ * `Object.fromEntries` is what makes a caller-supplied `"__proto__"` key safe:
1395
+ * it builds each key with CreateDataProperty, which defines an own property
1396
+ * instead of invoking the `__proto__` setter — the same guarantee the previous
1397
+ * explicit `Object.defineProperty` loop bought by hand.
1435
1398
  *
1436
1399
  * @internal
1437
1400
  */
1438
1401
  function foldRecord(results) {
1439
- let firstErr;
1440
- let firstDefect;
1441
- const values = {};
1442
- for (const [key, r] of Object.entries(results)) {
1443
- if (!isResult(r)) {
1444
- firstDefect ??= nonResultDefect();
1445
- break;
1446
- }
1447
- if (r.tag === "Defect") {
1448
- firstDefect ??= r;
1449
- break;
1450
- } else if (r.tag === "Err") firstErr ??= r;
1451
- else Object.defineProperty(values, key, {
1452
- value: r.value,
1453
- enumerable: true,
1454
- writable: true,
1455
- configurable: true
1456
- });
1457
- }
1458
- return firstDefect ?? firstErr ?? Ok(values);
1402
+ const keys = Object.keys(results);
1403
+ return foldArray(Object.values(results)).map((values) => Object.fromEntries(keys.map((key, i) => [key, values[i]])));
1459
1404
  }
1460
1405
  /**
1461
1406
  * Collect a tuple/array of {@link Result}s into a single `Result` of all their
@@ -1553,14 +1498,8 @@ function allAsync(results) {
1553
1498
  * ```
1554
1499
  */
1555
1500
  function allFromDictAsync(results) {
1556
- const entries = Object.entries(results);
1557
- return new AsyncRes(Promise.all(entries.map(([, ar]) => Promise.resolve(ar).then((x) => x, (cause) => defectRes(cause)))).then((resolved) => {
1558
- const byKey = Object.create(null);
1559
- entries.forEach(([key], i) => {
1560
- byKey[key] = resolved[i];
1561
- });
1562
- return foldRecord(byKey);
1563
- }));
1501
+ const keys = Object.keys(results);
1502
+ 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]])))));
1564
1503
  }
1565
1504
  //#endregion
1566
1505
  //#region src/facade.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unthrown",
3
- "version": "5.3.0",
3
+ "version": "5.5.0",
4
4
  "description": "Explicit errors as values, with a separate defect (panic) channel",
5
5
  "keywords": [
6
6
  "defect",