unthrown 5.0.0 → 5.2.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 +100 -26
- package/dist/index.d.cts +44 -10
- package/dist/index.d.mts +44 -10
- package/dist/index.mjs +100 -26
- package/package.json +4 -8
package/dist/index.cjs
CHANGED
|
@@ -25,7 +25,7 @@ var NonExhaustiveError = class extends Error {
|
|
|
25
25
|
constructor(input) {
|
|
26
26
|
let printed;
|
|
27
27
|
try {
|
|
28
|
-
printed = JSON.stringify(input);
|
|
28
|
+
printed = JSON.stringify(input) ?? String(input);
|
|
29
29
|
} catch {
|
|
30
30
|
printed = String(input);
|
|
31
31
|
}
|
|
@@ -261,7 +261,7 @@ var Res = class {
|
|
|
261
261
|
if (this.tag !== "Ok") return passThrough(this);
|
|
262
262
|
try {
|
|
263
263
|
const r = f(this.value);
|
|
264
|
-
return isResult(r) ? r : nonResultCallbackDefect();
|
|
264
|
+
return isResult(r) ? r : nonResultCallbackDefect(r);
|
|
265
265
|
} catch (cause) {
|
|
266
266
|
return defectRes(cause);
|
|
267
267
|
}
|
|
@@ -269,7 +269,7 @@ var Res = class {
|
|
|
269
269
|
tap(f) {
|
|
270
270
|
if (this.tag !== "Ok") return this;
|
|
271
271
|
try {
|
|
272
|
-
f(this.value);
|
|
272
|
+
silenceIfThenable(f(this.value));
|
|
273
273
|
return this;
|
|
274
274
|
} catch (cause) {
|
|
275
275
|
return defectRes(cause);
|
|
@@ -279,7 +279,7 @@ var Res = class {
|
|
|
279
279
|
if (this.tag !== "Ok") return this;
|
|
280
280
|
try {
|
|
281
281
|
const r = f(this.value);
|
|
282
|
-
if (!isResult(r)) return nonResultCallbackDefect();
|
|
282
|
+
if (!isResult(r)) return nonResultCallbackDefect(r);
|
|
283
283
|
return r.tag === "Ok" ? this : passThrough(r);
|
|
284
284
|
} catch (cause) {
|
|
285
285
|
return defectRes(cause);
|
|
@@ -289,7 +289,7 @@ var Res = class {
|
|
|
289
289
|
if (this.tag !== "Ok") return passThrough(this);
|
|
290
290
|
try {
|
|
291
291
|
const r = f(this.value);
|
|
292
|
-
if (!isResult(r)) return nonResultCallbackDefect();
|
|
292
|
+
if (!isResult(r)) return nonResultCallbackDefect(r);
|
|
293
293
|
if (r.tag !== "Ok") return passThrough(r);
|
|
294
294
|
return okRes({
|
|
295
295
|
...scopeOf(this.value),
|
|
@@ -341,7 +341,7 @@ var Res = class {
|
|
|
341
341
|
try {
|
|
342
342
|
const out = runMatch(f, this.error);
|
|
343
343
|
if (isDefectMarker(out)) return defectRes(out.cause);
|
|
344
|
-
if (!isResult(out)) return nonResultCallbackDefect();
|
|
344
|
+
if (!isResult(out)) return nonResultCallbackDefect(out);
|
|
345
345
|
return out;
|
|
346
346
|
} catch (cause) {
|
|
347
347
|
return defectRes(cause);
|
|
@@ -362,6 +362,7 @@ var Res = class {
|
|
|
362
362
|
try {
|
|
363
363
|
const out = runMatch(f, this.error);
|
|
364
364
|
if (isDefectMarker(out)) return observerThrowToDefect(out.cause, this.error);
|
|
365
|
+
silenceIfThenable(out);
|
|
365
366
|
return this;
|
|
366
367
|
} catch (cause) {
|
|
367
368
|
return observerThrowToDefect(cause, this.error);
|
|
@@ -372,7 +373,7 @@ var Res = class {
|
|
|
372
373
|
try {
|
|
373
374
|
const r = runMatch(f, this.error);
|
|
374
375
|
if (isDefectMarker(r)) return observerThrowToDefect(r.cause, this.error);
|
|
375
|
-
if (!isResult(r)) return nonResultCallbackDefect();
|
|
376
|
+
if (!isResult(r)) return nonResultCallbackDefect(r);
|
|
376
377
|
return r.tag === "Ok" ? this : passThrough(r);
|
|
377
378
|
} catch (cause) {
|
|
378
379
|
return observerThrowToDefect(cause, this.error);
|
|
@@ -382,7 +383,7 @@ var Res = class {
|
|
|
382
383
|
if (this.tag !== "Defect") return this;
|
|
383
384
|
try {
|
|
384
385
|
const r = f(this.cause);
|
|
385
|
-
return isResult(r) ? r : nonResultCallbackDefect();
|
|
386
|
+
return isResult(r) ? r : nonResultCallbackDefect(r);
|
|
386
387
|
} catch (cause) {
|
|
387
388
|
return defectRes(cause);
|
|
388
389
|
}
|
|
@@ -390,7 +391,7 @@ var Res = class {
|
|
|
390
391
|
tapDefect(f) {
|
|
391
392
|
if (this.tag !== "Defect") return this;
|
|
392
393
|
try {
|
|
393
|
-
f(this.cause);
|
|
394
|
+
silenceIfThenable(f(this.cause));
|
|
394
395
|
return this;
|
|
395
396
|
} catch (cause) {
|
|
396
397
|
return observerThrowToDefect(cause, this.cause);
|
|
@@ -399,7 +400,7 @@ var Res = class {
|
|
|
399
400
|
tapFailure(f) {
|
|
400
401
|
if (this.tag === "Ok") return this;
|
|
401
402
|
try {
|
|
402
|
-
f(this);
|
|
403
|
+
silenceIfThenable(f(this));
|
|
403
404
|
return this;
|
|
404
405
|
} catch (cause) {
|
|
405
406
|
return observerThrowToDefect(cause, this.tag === "Err" ? this.error : this.cause);
|
|
@@ -537,7 +538,11 @@ function defectRes(cause) {
|
|
|
537
538
|
* // `E` is `unknown` here — an untyped boundary has no cases to enumerate,
|
|
538
539
|
* // so the `P._` escape hatch is the only arm that can terminate the match:
|
|
539
540
|
* // oxlint-disable-next-line unthrown/no-catch-all-pattern -- untyped boundary: `E` is `unknown`
|
|
540
|
-
* x.match({
|
|
541
|
+
* x.match({
|
|
542
|
+
* ok: () => 1,
|
|
543
|
+
* errCases: (m) => m.with(P._, () => 0),
|
|
544
|
+
* defect: () => -1,
|
|
545
|
+
* });
|
|
541
546
|
* ```
|
|
542
547
|
*
|
|
543
548
|
* @category Guards
|
|
@@ -564,6 +569,30 @@ function passThrough(self) {
|
|
|
564
569
|
return self;
|
|
565
570
|
}
|
|
566
571
|
/**
|
|
572
|
+
* Adopt-and-silence a thenable a combinator is about to **discard**.
|
|
573
|
+
*
|
|
574
|
+
* @remarks
|
|
575
|
+
* The observers (`tap`, `tapErrCases`, `tapDefect`, `tapFailure`) throw their
|
|
576
|
+
* callback's return value away, and the `Result`-returning combinators reject a
|
|
577
|
+
* non-`Result` one. Either way, a thenable that slipped past `NotThenable` (a
|
|
578
|
+
* cast, a raw-JS caller) is dropped while still in flight — and if it later
|
|
579
|
+
* rejects, nothing is holding it, so the rejection floats unhandled and takes
|
|
580
|
+
* the process down on Node by default. Worse for an observer: its whole job is
|
|
581
|
+
* to make a failure visible, and this is the one path where the failure is
|
|
582
|
+
* invisible.
|
|
583
|
+
*
|
|
584
|
+
* Adopting it costs one microtask and makes the rejection a no-op. The
|
|
585
|
+
* boundaries already do exactly this for a thenable `qualify` and a thenable
|
|
586
|
+
* `fn` (see `interop.ts`); this is the same net on the combinator side.
|
|
587
|
+
*
|
|
588
|
+
* @internal
|
|
589
|
+
*/
|
|
590
|
+
function silenceIfThenable(value) {
|
|
591
|
+
try {
|
|
592
|
+
if ((typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function") Promise.resolve(value).then(void 0, () => void 0);
|
|
593
|
+
} catch {}
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
567
596
|
* The Defect minted when a callback constrained to return a `Result` returns
|
|
568
597
|
* something else — reachable only from untyped/cast callers (in typed code the
|
|
569
598
|
* constraint is a compile error). The combinator-side sibling of the
|
|
@@ -573,7 +602,8 @@ function passThrough(self) {
|
|
|
573
602
|
*
|
|
574
603
|
* @internal
|
|
575
604
|
*/
|
|
576
|
-
function nonResultCallbackDefect() {
|
|
605
|
+
function nonResultCallbackDefect(returned) {
|
|
606
|
+
silenceIfThenable(returned);
|
|
577
607
|
return defectRes(/* @__PURE__ */ new TypeError("unthrown: a combinator callback returned a non-Result value"));
|
|
578
608
|
}
|
|
579
609
|
/**
|
|
@@ -658,7 +688,7 @@ var AsyncRes = class AsyncRes {
|
|
|
658
688
|
if (r.tag !== "Ok") return passThrough(r);
|
|
659
689
|
try {
|
|
660
690
|
const inner = await f(r.value);
|
|
661
|
-
return isResult(inner) ? inner : nonResultCallbackDefect();
|
|
691
|
+
return isResult(inner) ? inner : nonResultCallbackDefect(inner);
|
|
662
692
|
} catch (cause) {
|
|
663
693
|
return defectRes(cause);
|
|
664
694
|
}
|
|
@@ -668,7 +698,7 @@ var AsyncRes = class AsyncRes {
|
|
|
668
698
|
return new AsyncRes(this.#promise.then((r) => {
|
|
669
699
|
if (r.tag !== "Ok") return r;
|
|
670
700
|
try {
|
|
671
|
-
f(r.value);
|
|
701
|
+
silenceIfThenable(f(r.value));
|
|
672
702
|
return r;
|
|
673
703
|
} catch (cause) {
|
|
674
704
|
return defectRes(cause);
|
|
@@ -680,7 +710,7 @@ var AsyncRes = class AsyncRes {
|
|
|
680
710
|
if (r.tag !== "Ok") return passThrough(r);
|
|
681
711
|
try {
|
|
682
712
|
const inner = await f(r.value);
|
|
683
|
-
if (!isResult(inner)) return nonResultCallbackDefect();
|
|
713
|
+
if (!isResult(inner)) return nonResultCallbackDefect(inner);
|
|
684
714
|
return inner.tag === "Ok" ? r : passThrough(inner);
|
|
685
715
|
} catch (cause) {
|
|
686
716
|
return defectRes(cause);
|
|
@@ -692,7 +722,7 @@ var AsyncRes = class AsyncRes {
|
|
|
692
722
|
if (r.tag !== "Ok") return passThrough(r);
|
|
693
723
|
try {
|
|
694
724
|
const inner = await f(r.value);
|
|
695
|
-
if (!isResult(inner)) return nonResultCallbackDefect();
|
|
725
|
+
if (!isResult(inner)) return nonResultCallbackDefect(inner);
|
|
696
726
|
if (inner.tag !== "Ok") return passThrough(inner);
|
|
697
727
|
return okRes({
|
|
698
728
|
...scopeOf(r.value),
|
|
@@ -751,7 +781,7 @@ var AsyncRes = class AsyncRes {
|
|
|
751
781
|
const out = runMatch(f, r.error);
|
|
752
782
|
if (isDefectMarker(out)) return defectRes(out.cause);
|
|
753
783
|
const inner = await out;
|
|
754
|
-
if (!isResult(inner)) return nonResultCallbackDefect();
|
|
784
|
+
if (!isResult(inner)) return nonResultCallbackDefect(inner);
|
|
755
785
|
return inner;
|
|
756
786
|
} catch (cause) {
|
|
757
787
|
return defectRes(cause);
|
|
@@ -776,6 +806,7 @@ var AsyncRes = class AsyncRes {
|
|
|
776
806
|
try {
|
|
777
807
|
const out = runMatch(f, r.error);
|
|
778
808
|
if (isDefectMarker(out)) return observerThrowToDefect(out.cause, r.error);
|
|
809
|
+
silenceIfThenable(out);
|
|
779
810
|
return r;
|
|
780
811
|
} catch (cause) {
|
|
781
812
|
return observerThrowToDefect(cause, r.error);
|
|
@@ -789,7 +820,7 @@ var AsyncRes = class AsyncRes {
|
|
|
789
820
|
const out = runMatch(f, r.error);
|
|
790
821
|
if (isDefectMarker(out)) return observerThrowToDefect(out.cause, r.error);
|
|
791
822
|
const inner = await out;
|
|
792
|
-
if (!isResult(inner)) return nonResultCallbackDefect();
|
|
823
|
+
if (!isResult(inner)) return nonResultCallbackDefect(inner);
|
|
793
824
|
return inner.tag === "Ok" ? passThrough(r) : passThrough(inner);
|
|
794
825
|
} catch (cause) {
|
|
795
826
|
return observerThrowToDefect(cause, r.error);
|
|
@@ -801,7 +832,7 @@ var AsyncRes = class AsyncRes {
|
|
|
801
832
|
if (r.tag !== "Defect") return r;
|
|
802
833
|
try {
|
|
803
834
|
const inner = await f(r.cause);
|
|
804
|
-
return isResult(inner) ? inner : nonResultCallbackDefect();
|
|
835
|
+
return isResult(inner) ? inner : nonResultCallbackDefect(inner);
|
|
805
836
|
} catch (cause) {
|
|
806
837
|
return defectRes(cause);
|
|
807
838
|
}
|
|
@@ -811,7 +842,7 @@ var AsyncRes = class AsyncRes {
|
|
|
811
842
|
return new AsyncRes(this.#promise.then((r) => {
|
|
812
843
|
if (r.tag !== "Defect") return r;
|
|
813
844
|
try {
|
|
814
|
-
f(r.cause);
|
|
845
|
+
silenceIfThenable(f(r.cause));
|
|
815
846
|
return r;
|
|
816
847
|
} catch (cause) {
|
|
817
848
|
return observerThrowToDefect(cause, r.cause);
|
|
@@ -822,7 +853,7 @@ var AsyncRes = class AsyncRes {
|
|
|
822
853
|
return new AsyncRes(this.#promise.then((r) => {
|
|
823
854
|
if (r.tag === "Ok") return r;
|
|
824
855
|
try {
|
|
825
|
-
f(r);
|
|
856
|
+
silenceIfThenable(f(r));
|
|
826
857
|
return r;
|
|
827
858
|
} catch (cause) {
|
|
828
859
|
return observerThrowToDefect(cause, r.tag === "Err" ? r.error : r.cause);
|
|
@@ -1090,6 +1121,11 @@ function fromNullable(value, onAbsent) {
|
|
|
1090
1121
|
* — and a thenable slipped past the types at runtime becomes a `Defect` (never
|
|
1091
1122
|
* an `Err(Promise)`), its orphaned rejection silenced.
|
|
1092
1123
|
*
|
|
1124
|
+
* `fn` is **synchronous** too. An `async` `fn` rejects *after* this boundary has
|
|
1125
|
+
* already returned, so its rejection could never reach `qualify`: it becomes a
|
|
1126
|
+
* `Defect` (never `Ok(<Promise>)`) and the orphaned rejection is silenced rather
|
|
1127
|
+
* than left to float. Reach for {@link fromPromise} to wrap async work.
|
|
1128
|
+
*
|
|
1093
1129
|
* The modeled error type is `Exclude<R, Defect>` — the `Defect` arm of
|
|
1094
1130
|
* `qualify`'s return is **subtracted** from `E`, never inferred into it. So a
|
|
1095
1131
|
* `qualify` that returns *only* `defect(cause)` yields `E = never` (a Defect is
|
|
@@ -1126,7 +1162,8 @@ function fromThrowable(fn, qualify) {
|
|
|
1126
1162
|
const triage = qualify;
|
|
1127
1163
|
return (...args) => {
|
|
1128
1164
|
try {
|
|
1129
|
-
|
|
1165
|
+
const value = fn(...args);
|
|
1166
|
+
return isThenable(value) ? thenableReturnDefect(value) : Ok(value);
|
|
1130
1167
|
} catch (cause) {
|
|
1131
1168
|
return qualifyToResult(cause, triage);
|
|
1132
1169
|
}
|
|
@@ -1143,6 +1180,10 @@ function fromThrowable(fn, qualify) {
|
|
|
1143
1180
|
* `qualify`. When some throws *are* anticipated, reach for
|
|
1144
1181
|
* {@link fromThrowable} and triage them.
|
|
1145
1182
|
*
|
|
1183
|
+
* `fn` is **synchronous**: an `async` `fn` becomes a `Defect` (never
|
|
1184
|
+
* `Ok(<Promise>)`), with its orphaned rejection silenced rather than left to
|
|
1185
|
+
* float. Reach for {@link fromSafePromise} to wrap async work.
|
|
1186
|
+
*
|
|
1146
1187
|
* @typeParam A - the wrapped function's argument tuple.
|
|
1147
1188
|
* @typeParam T - the wrapped function's return type.
|
|
1148
1189
|
* @param fn - the throwing function to wrap.
|
|
@@ -1164,7 +1205,8 @@ function fromThrowable(fn, qualify) {
|
|
|
1164
1205
|
function fromSafeThrowable(fn) {
|
|
1165
1206
|
return (...args) => {
|
|
1166
1207
|
try {
|
|
1167
|
-
|
|
1208
|
+
const value = fn(...args);
|
|
1209
|
+
return isThenable(value) ? thenableReturnDefect(value) : Ok(value);
|
|
1168
1210
|
} catch (cause) {
|
|
1169
1211
|
return defectRes(cause);
|
|
1170
1212
|
}
|
|
@@ -1260,7 +1302,33 @@ function qualifyToResult(cause, qualify) {
|
|
|
1260
1302
|
}
|
|
1261
1303
|
}
|
|
1262
1304
|
/**
|
|
1263
|
-
*
|
|
1305
|
+
* The Defect minted when a **synchronous** boundary's `fn` returns a thenable —
|
|
1306
|
+
* i.e. an `async` function was handed to {@link fromThrowable} /
|
|
1307
|
+
* {@link fromSafeThrowable}.
|
|
1308
|
+
*
|
|
1309
|
+
* @remarks
|
|
1310
|
+
* This is the sibling of the thenable-`qualify` net in {@link qualifyToResult},
|
|
1311
|
+
* and it closes a strictly worse hole. A synchronous boundary only ever sees a
|
|
1312
|
+
* synchronous `throw`, so an async `fn`'s rejection never reaches `qualify` at
|
|
1313
|
+
* all: it would sit inside `Ok(<Promise>)`, un-triaged, and then float as an
|
|
1314
|
+
* unhandled rejection — which terminates the process on Node by default.
|
|
1315
|
+
*
|
|
1316
|
+
* Unlike the combinator callbacks, this cannot be banned at compile time
|
|
1317
|
+
* without collateral damage: `T & NotThenable<T>` on `fn`'s return makes a
|
|
1318
|
+
* **generic** function unassignable, so `fromSafeThrowable(structuredClone)`
|
|
1319
|
+
* stops compiling and `T` collapses to `unknown`. (The phantom rest-tuple guard
|
|
1320
|
+
* `fromPromise` uses fares worse.) So the ban is enforced here, at runtime,
|
|
1321
|
+
* where it costs nothing: a Defect, plus adopt-and-silence so the orphaned
|
|
1322
|
+
* rejection cannot float.
|
|
1323
|
+
*
|
|
1324
|
+
* @internal
|
|
1325
|
+
*/
|
|
1326
|
+
function thenableReturnDefect(value) {
|
|
1327
|
+
Promise.resolve(value).then(void 0, () => void 0);
|
|
1328
|
+
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."));
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Runtime thenable probe for the belt-and-braces guards above. Called inside the
|
|
1264
1332
|
* caller's `try`, so even a hostile `.then` getter lands on the Defect path.
|
|
1265
1333
|
*
|
|
1266
1334
|
* @internal
|
|
@@ -1388,7 +1456,10 @@ function allFromDict(results) {
|
|
|
1388
1456
|
* ```ts
|
|
1389
1457
|
* import { allAsync, fromSafePromise } from "unthrown";
|
|
1390
1458
|
*
|
|
1391
|
-
* const both = allAsync([
|
|
1459
|
+
* const both = allAsync([
|
|
1460
|
+
* fromSafePromise(Promise.resolve(1)),
|
|
1461
|
+
* fromSafePromise(Promise.resolve(2)),
|
|
1462
|
+
* ]);
|
|
1392
1463
|
* (await both).get(); // => [1, 2]
|
|
1393
1464
|
* ```
|
|
1394
1465
|
*/
|
|
@@ -1492,7 +1563,10 @@ const Result = {
|
|
|
1492
1563
|
* @example
|
|
1493
1564
|
* ```ts
|
|
1494
1565
|
* import { AsyncResult } from "unthrown";
|
|
1495
|
-
* const user = await AsyncResult.fromPromise(
|
|
1566
|
+
* const user = await AsyncResult.fromPromise(
|
|
1567
|
+
* fetchUser(id),
|
|
1568
|
+
* (c, defect) => defect(c),
|
|
1569
|
+
* );
|
|
1496
1570
|
* user.get(); // => the fetched user (on success)
|
|
1497
1571
|
* ```
|
|
1498
1572
|
*/
|
package/dist/index.d.cts
CHANGED
|
@@ -296,10 +296,17 @@ type Bound<T, K extends string, U> = Prettify<Omit<T, K> & { readonly [P in K]:
|
|
|
296
296
|
* would escape the pipeline as an unhandled rejection instead of a `Defect`.
|
|
297
297
|
* Lift async work with {@link fromPromise} and compose it with `flatMap`.
|
|
298
298
|
*
|
|
299
|
+
* Spelled with `Extract`, not `[R] extends [PromiseLike<…>]`, so the ban also
|
|
300
|
+
* fires when only SOME arms of a union return are thenable — a *sometimes*-async
|
|
301
|
+
* callback (`flag ? 1 : work()`) is still an unawaited effect whose rejection
|
|
302
|
+
* the pipeline never sees. The tuple-wrapped form is false for a partial union
|
|
303
|
+
* and let exactly that through. This is the same reasoning `fromPromise`'s
|
|
304
|
+
* async-qualify guard already used.
|
|
305
|
+
*
|
|
299
306
|
* @typeParam R - the callback's inferred return type.
|
|
300
307
|
* @category Types
|
|
301
308
|
*/
|
|
302
|
-
type NotThenable<R> = [R] extends [
|
|
309
|
+
type NotThenable<R> = [Extract<R, PromiseLike<unknown>>] extends [never] ? unknown : "unthrown: combinator callbacks are synchronous — lift async work with fromPromise and compose with flatMap";
|
|
303
310
|
/**
|
|
304
311
|
* The built-in match builder over an error union `E`, as produced by
|
|
305
312
|
* `match(error)`. This is what an error combinator's callback receives — chain
|
|
@@ -759,12 +766,20 @@ type ResultMethods<out T, out E> = {
|
|
|
759
766
|
*
|
|
760
767
|
* @remarks
|
|
761
768
|
* A deliberate escape hatch off the errors-as-values model — it **throws the
|
|
762
|
-
* `Err` value as-is** at the call site
|
|
763
|
-
*
|
|
764
|
-
* this
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
769
|
+
* `Err` value as-is** at the call site, so a caller of the enclosing function
|
|
770
|
+
* sees a throw rather than a channel. Its home is **tests and scripts**,
|
|
771
|
+
* where "this `Result` had better be `Ok`" is the assertion and a throw is
|
|
772
|
+
* the correct failure mode.
|
|
773
|
+
*
|
|
774
|
+
* In production code, fold the error channel instead:
|
|
775
|
+
* {@link ResultMethods.recoverErrCases | recoverErrCases} empties `E`, so
|
|
776
|
+
* {@link ResultMethods.get | get} compiles and a case routed to the injected
|
|
777
|
+
* `defect(...)` panics with its original cause — with every case still named.
|
|
778
|
+
* {@link ResultMethods.match | match} and
|
|
779
|
+
* {@link ResultMethods.flatMapErrCases | flatMapErrCases} are the other two
|
|
780
|
+
* ways to keep the error a value. `@unthrown/oxlint`'s opt-in
|
|
781
|
+
* `no-get-or-throw` rule enforces this, exempting test files through an
|
|
782
|
+
* oxlint `overrides` entry.
|
|
768
783
|
*
|
|
769
784
|
* Type-gated as the **complement** of {@link ResultMethods.get | get}: it
|
|
770
785
|
* compiles only when the error channel is **non-empty** (`E` is not `never`) —
|
|
@@ -1443,7 +1458,11 @@ declare class GetError<E = unknown> extends Error {
|
|
|
1443
1458
|
* // `E` is `unknown` here — an untyped boundary has no cases to enumerate,
|
|
1444
1459
|
* // so the `P._` escape hatch is the only arm that can terminate the match:
|
|
1445
1460
|
* // oxlint-disable-next-line unthrown/no-catch-all-pattern -- untyped boundary: `E` is `unknown`
|
|
1446
|
-
* x.match({
|
|
1461
|
+
* x.match({
|
|
1462
|
+
* ok: () => 1,
|
|
1463
|
+
* errCases: (m) => m.with(P._, () => 0),
|
|
1464
|
+
* defect: () => -1,
|
|
1465
|
+
* });
|
|
1447
1466
|
* ```
|
|
1448
1467
|
*
|
|
1449
1468
|
* @category Guards
|
|
@@ -1558,6 +1577,11 @@ declare function fromNullable<T, E>(value: T | null | undefined, onAbsent: () =>
|
|
|
1558
1577
|
* — and a thenable slipped past the types at runtime becomes a `Defect` (never
|
|
1559
1578
|
* an `Err(Promise)`), its orphaned rejection silenced.
|
|
1560
1579
|
*
|
|
1580
|
+
* `fn` is **synchronous** too. An `async` `fn` rejects *after* this boundary has
|
|
1581
|
+
* already returned, so its rejection could never reach `qualify`: it becomes a
|
|
1582
|
+
* `Defect` (never `Ok(<Promise>)`) and the orphaned rejection is silenced rather
|
|
1583
|
+
* than left to float. Reach for {@link fromPromise} to wrap async work.
|
|
1584
|
+
*
|
|
1561
1585
|
* The modeled error type is `Exclude<R, Defect>` — the `Defect` arm of
|
|
1562
1586
|
* `qualify`'s return is **subtracted** from `E`, never inferred into it. So a
|
|
1563
1587
|
* `qualify` that returns *only* `defect(cause)` yields `E = never` (a Defect is
|
|
@@ -1602,6 +1626,10 @@ declare function fromThrowable<A extends unknown[], T, R>(fn: (...args: A) => T,
|
|
|
1602
1626
|
* `qualify`. When some throws *are* anticipated, reach for
|
|
1603
1627
|
* {@link fromThrowable} and triage them.
|
|
1604
1628
|
*
|
|
1629
|
+
* `fn` is **synchronous**: an `async` `fn` becomes a `Defect` (never
|
|
1630
|
+
* `Ok(<Promise>)`), with its orphaned rejection silenced rather than left to
|
|
1631
|
+
* float. Reach for {@link fromSafePromise} to wrap async work.
|
|
1632
|
+
*
|
|
1605
1633
|
* @typeParam A - the wrapped function's argument tuple.
|
|
1606
1634
|
* @typeParam T - the wrapped function's return type.
|
|
1607
1635
|
* @param fn - the throwing function to wrap.
|
|
@@ -1774,7 +1802,10 @@ declare function allFromDict<R extends ResultRecord>(results: R): Result$1<{ [K
|
|
|
1774
1802
|
* ```ts
|
|
1775
1803
|
* import { allAsync, fromSafePromise } from "unthrown";
|
|
1776
1804
|
*
|
|
1777
|
-
* const both = allAsync([
|
|
1805
|
+
* const both = allAsync([
|
|
1806
|
+
* fromSafePromise(Promise.resolve(1)),
|
|
1807
|
+
* fromSafePromise(Promise.resolve(2)),
|
|
1808
|
+
* ]);
|
|
1778
1809
|
* (await both).get(); // => [1, 2]
|
|
1779
1810
|
* ```
|
|
1780
1811
|
*/
|
|
@@ -1882,7 +1913,10 @@ type Result<T, E> = Result$1<T, E>;
|
|
|
1882
1913
|
* @example
|
|
1883
1914
|
* ```ts
|
|
1884
1915
|
* import { AsyncResult } from "unthrown";
|
|
1885
|
-
* const user = await AsyncResult.fromPromise(
|
|
1916
|
+
* const user = await AsyncResult.fromPromise(
|
|
1917
|
+
* fetchUser(id),
|
|
1918
|
+
* (c, defect) => defect(c),
|
|
1919
|
+
* );
|
|
1886
1920
|
* user.get(); // => the fetched user (on success)
|
|
1887
1921
|
* ```
|
|
1888
1922
|
*/
|
package/dist/index.d.mts
CHANGED
|
@@ -296,10 +296,17 @@ type Bound<T, K extends string, U> = Prettify<Omit<T, K> & { readonly [P in K]:
|
|
|
296
296
|
* would escape the pipeline as an unhandled rejection instead of a `Defect`.
|
|
297
297
|
* Lift async work with {@link fromPromise} and compose it with `flatMap`.
|
|
298
298
|
*
|
|
299
|
+
* Spelled with `Extract`, not `[R] extends [PromiseLike<…>]`, so the ban also
|
|
300
|
+
* fires when only SOME arms of a union return are thenable — a *sometimes*-async
|
|
301
|
+
* callback (`flag ? 1 : work()`) is still an unawaited effect whose rejection
|
|
302
|
+
* the pipeline never sees. The tuple-wrapped form is false for a partial union
|
|
303
|
+
* and let exactly that through. This is the same reasoning `fromPromise`'s
|
|
304
|
+
* async-qualify guard already used.
|
|
305
|
+
*
|
|
299
306
|
* @typeParam R - the callback's inferred return type.
|
|
300
307
|
* @category Types
|
|
301
308
|
*/
|
|
302
|
-
type NotThenable<R> = [R] extends [
|
|
309
|
+
type NotThenable<R> = [Extract<R, PromiseLike<unknown>>] extends [never] ? unknown : "unthrown: combinator callbacks are synchronous — lift async work with fromPromise and compose with flatMap";
|
|
303
310
|
/**
|
|
304
311
|
* The built-in match builder over an error union `E`, as produced by
|
|
305
312
|
* `match(error)`. This is what an error combinator's callback receives — chain
|
|
@@ -759,12 +766,20 @@ type ResultMethods<out T, out E> = {
|
|
|
759
766
|
*
|
|
760
767
|
* @remarks
|
|
761
768
|
* A deliberate escape hatch off the errors-as-values model — it **throws the
|
|
762
|
-
* `Err` value as-is** at the call site
|
|
763
|
-
*
|
|
764
|
-
* this
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
769
|
+
* `Err` value as-is** at the call site, so a caller of the enclosing function
|
|
770
|
+
* sees a throw rather than a channel. Its home is **tests and scripts**,
|
|
771
|
+
* where "this `Result` had better be `Ok`" is the assertion and a throw is
|
|
772
|
+
* the correct failure mode.
|
|
773
|
+
*
|
|
774
|
+
* In production code, fold the error channel instead:
|
|
775
|
+
* {@link ResultMethods.recoverErrCases | recoverErrCases} empties `E`, so
|
|
776
|
+
* {@link ResultMethods.get | get} compiles and a case routed to the injected
|
|
777
|
+
* `defect(...)` panics with its original cause — with every case still named.
|
|
778
|
+
* {@link ResultMethods.match | match} and
|
|
779
|
+
* {@link ResultMethods.flatMapErrCases | flatMapErrCases} are the other two
|
|
780
|
+
* ways to keep the error a value. `@unthrown/oxlint`'s opt-in
|
|
781
|
+
* `no-get-or-throw` rule enforces this, exempting test files through an
|
|
782
|
+
* oxlint `overrides` entry.
|
|
768
783
|
*
|
|
769
784
|
* Type-gated as the **complement** of {@link ResultMethods.get | get}: it
|
|
770
785
|
* compiles only when the error channel is **non-empty** (`E` is not `never`) —
|
|
@@ -1443,7 +1458,11 @@ declare class GetError<E = unknown> extends Error {
|
|
|
1443
1458
|
* // `E` is `unknown` here — an untyped boundary has no cases to enumerate,
|
|
1444
1459
|
* // so the `P._` escape hatch is the only arm that can terminate the match:
|
|
1445
1460
|
* // oxlint-disable-next-line unthrown/no-catch-all-pattern -- untyped boundary: `E` is `unknown`
|
|
1446
|
-
* x.match({
|
|
1461
|
+
* x.match({
|
|
1462
|
+
* ok: () => 1,
|
|
1463
|
+
* errCases: (m) => m.with(P._, () => 0),
|
|
1464
|
+
* defect: () => -1,
|
|
1465
|
+
* });
|
|
1447
1466
|
* ```
|
|
1448
1467
|
*
|
|
1449
1468
|
* @category Guards
|
|
@@ -1558,6 +1577,11 @@ declare function fromNullable<T, E>(value: T | null | undefined, onAbsent: () =>
|
|
|
1558
1577
|
* — and a thenable slipped past the types at runtime becomes a `Defect` (never
|
|
1559
1578
|
* an `Err(Promise)`), its orphaned rejection silenced.
|
|
1560
1579
|
*
|
|
1580
|
+
* `fn` is **synchronous** too. An `async` `fn` rejects *after* this boundary has
|
|
1581
|
+
* already returned, so its rejection could never reach `qualify`: it becomes a
|
|
1582
|
+
* `Defect` (never `Ok(<Promise>)`) and the orphaned rejection is silenced rather
|
|
1583
|
+
* than left to float. Reach for {@link fromPromise} to wrap async work.
|
|
1584
|
+
*
|
|
1561
1585
|
* The modeled error type is `Exclude<R, Defect>` — the `Defect` arm of
|
|
1562
1586
|
* `qualify`'s return is **subtracted** from `E`, never inferred into it. So a
|
|
1563
1587
|
* `qualify` that returns *only* `defect(cause)` yields `E = never` (a Defect is
|
|
@@ -1602,6 +1626,10 @@ declare function fromThrowable<A extends unknown[], T, R>(fn: (...args: A) => T,
|
|
|
1602
1626
|
* `qualify`. When some throws *are* anticipated, reach for
|
|
1603
1627
|
* {@link fromThrowable} and triage them.
|
|
1604
1628
|
*
|
|
1629
|
+
* `fn` is **synchronous**: an `async` `fn` becomes a `Defect` (never
|
|
1630
|
+
* `Ok(<Promise>)`), with its orphaned rejection silenced rather than left to
|
|
1631
|
+
* float. Reach for {@link fromSafePromise} to wrap async work.
|
|
1632
|
+
*
|
|
1605
1633
|
* @typeParam A - the wrapped function's argument tuple.
|
|
1606
1634
|
* @typeParam T - the wrapped function's return type.
|
|
1607
1635
|
* @param fn - the throwing function to wrap.
|
|
@@ -1774,7 +1802,10 @@ declare function allFromDict<R extends ResultRecord>(results: R): Result$1<{ [K
|
|
|
1774
1802
|
* ```ts
|
|
1775
1803
|
* import { allAsync, fromSafePromise } from "unthrown";
|
|
1776
1804
|
*
|
|
1777
|
-
* const both = allAsync([
|
|
1805
|
+
* const both = allAsync([
|
|
1806
|
+
* fromSafePromise(Promise.resolve(1)),
|
|
1807
|
+
* fromSafePromise(Promise.resolve(2)),
|
|
1808
|
+
* ]);
|
|
1778
1809
|
* (await both).get(); // => [1, 2]
|
|
1779
1810
|
* ```
|
|
1780
1811
|
*/
|
|
@@ -1882,7 +1913,10 @@ type Result<T, E> = Result$1<T, E>;
|
|
|
1882
1913
|
* @example
|
|
1883
1914
|
* ```ts
|
|
1884
1915
|
* import { AsyncResult } from "unthrown";
|
|
1885
|
-
* const user = await AsyncResult.fromPromise(
|
|
1916
|
+
* const user = await AsyncResult.fromPromise(
|
|
1917
|
+
* fetchUser(id),
|
|
1918
|
+
* (c, defect) => defect(c),
|
|
1919
|
+
* );
|
|
1886
1920
|
* user.get(); // => the fetched user (on success)
|
|
1887
1921
|
* ```
|
|
1888
1922
|
*/
|
package/dist/index.mjs
CHANGED
|
@@ -24,7 +24,7 @@ var NonExhaustiveError = class extends Error {
|
|
|
24
24
|
constructor(input) {
|
|
25
25
|
let printed;
|
|
26
26
|
try {
|
|
27
|
-
printed = JSON.stringify(input);
|
|
27
|
+
printed = JSON.stringify(input) ?? String(input);
|
|
28
28
|
} catch {
|
|
29
29
|
printed = String(input);
|
|
30
30
|
}
|
|
@@ -260,7 +260,7 @@ var Res = class {
|
|
|
260
260
|
if (this.tag !== "Ok") return passThrough(this);
|
|
261
261
|
try {
|
|
262
262
|
const r = f(this.value);
|
|
263
|
-
return isResult(r) ? r : nonResultCallbackDefect();
|
|
263
|
+
return isResult(r) ? r : nonResultCallbackDefect(r);
|
|
264
264
|
} catch (cause) {
|
|
265
265
|
return defectRes(cause);
|
|
266
266
|
}
|
|
@@ -268,7 +268,7 @@ var Res = class {
|
|
|
268
268
|
tap(f) {
|
|
269
269
|
if (this.tag !== "Ok") return this;
|
|
270
270
|
try {
|
|
271
|
-
f(this.value);
|
|
271
|
+
silenceIfThenable(f(this.value));
|
|
272
272
|
return this;
|
|
273
273
|
} catch (cause) {
|
|
274
274
|
return defectRes(cause);
|
|
@@ -278,7 +278,7 @@ var Res = class {
|
|
|
278
278
|
if (this.tag !== "Ok") return this;
|
|
279
279
|
try {
|
|
280
280
|
const r = f(this.value);
|
|
281
|
-
if (!isResult(r)) return nonResultCallbackDefect();
|
|
281
|
+
if (!isResult(r)) return nonResultCallbackDefect(r);
|
|
282
282
|
return r.tag === "Ok" ? this : passThrough(r);
|
|
283
283
|
} catch (cause) {
|
|
284
284
|
return defectRes(cause);
|
|
@@ -288,7 +288,7 @@ var Res = class {
|
|
|
288
288
|
if (this.tag !== "Ok") return passThrough(this);
|
|
289
289
|
try {
|
|
290
290
|
const r = f(this.value);
|
|
291
|
-
if (!isResult(r)) return nonResultCallbackDefect();
|
|
291
|
+
if (!isResult(r)) return nonResultCallbackDefect(r);
|
|
292
292
|
if (r.tag !== "Ok") return passThrough(r);
|
|
293
293
|
return okRes({
|
|
294
294
|
...scopeOf(this.value),
|
|
@@ -340,7 +340,7 @@ var Res = class {
|
|
|
340
340
|
try {
|
|
341
341
|
const out = runMatch(f, this.error);
|
|
342
342
|
if (isDefectMarker(out)) return defectRes(out.cause);
|
|
343
|
-
if (!isResult(out)) return nonResultCallbackDefect();
|
|
343
|
+
if (!isResult(out)) return nonResultCallbackDefect(out);
|
|
344
344
|
return out;
|
|
345
345
|
} catch (cause) {
|
|
346
346
|
return defectRes(cause);
|
|
@@ -361,6 +361,7 @@ var Res = class {
|
|
|
361
361
|
try {
|
|
362
362
|
const out = runMatch(f, this.error);
|
|
363
363
|
if (isDefectMarker(out)) return observerThrowToDefect(out.cause, this.error);
|
|
364
|
+
silenceIfThenable(out);
|
|
364
365
|
return this;
|
|
365
366
|
} catch (cause) {
|
|
366
367
|
return observerThrowToDefect(cause, this.error);
|
|
@@ -371,7 +372,7 @@ var Res = class {
|
|
|
371
372
|
try {
|
|
372
373
|
const r = runMatch(f, this.error);
|
|
373
374
|
if (isDefectMarker(r)) return observerThrowToDefect(r.cause, this.error);
|
|
374
|
-
if (!isResult(r)) return nonResultCallbackDefect();
|
|
375
|
+
if (!isResult(r)) return nonResultCallbackDefect(r);
|
|
375
376
|
return r.tag === "Ok" ? this : passThrough(r);
|
|
376
377
|
} catch (cause) {
|
|
377
378
|
return observerThrowToDefect(cause, this.error);
|
|
@@ -381,7 +382,7 @@ var Res = class {
|
|
|
381
382
|
if (this.tag !== "Defect") return this;
|
|
382
383
|
try {
|
|
383
384
|
const r = f(this.cause);
|
|
384
|
-
return isResult(r) ? r : nonResultCallbackDefect();
|
|
385
|
+
return isResult(r) ? r : nonResultCallbackDefect(r);
|
|
385
386
|
} catch (cause) {
|
|
386
387
|
return defectRes(cause);
|
|
387
388
|
}
|
|
@@ -389,7 +390,7 @@ var Res = class {
|
|
|
389
390
|
tapDefect(f) {
|
|
390
391
|
if (this.tag !== "Defect") return this;
|
|
391
392
|
try {
|
|
392
|
-
f(this.cause);
|
|
393
|
+
silenceIfThenable(f(this.cause));
|
|
393
394
|
return this;
|
|
394
395
|
} catch (cause) {
|
|
395
396
|
return observerThrowToDefect(cause, this.cause);
|
|
@@ -398,7 +399,7 @@ var Res = class {
|
|
|
398
399
|
tapFailure(f) {
|
|
399
400
|
if (this.tag === "Ok") return this;
|
|
400
401
|
try {
|
|
401
|
-
f(this);
|
|
402
|
+
silenceIfThenable(f(this));
|
|
402
403
|
return this;
|
|
403
404
|
} catch (cause) {
|
|
404
405
|
return observerThrowToDefect(cause, this.tag === "Err" ? this.error : this.cause);
|
|
@@ -536,7 +537,11 @@ function defectRes(cause) {
|
|
|
536
537
|
* // `E` is `unknown` here — an untyped boundary has no cases to enumerate,
|
|
537
538
|
* // so the `P._` escape hatch is the only arm that can terminate the match:
|
|
538
539
|
* // oxlint-disable-next-line unthrown/no-catch-all-pattern -- untyped boundary: `E` is `unknown`
|
|
539
|
-
* x.match({
|
|
540
|
+
* x.match({
|
|
541
|
+
* ok: () => 1,
|
|
542
|
+
* errCases: (m) => m.with(P._, () => 0),
|
|
543
|
+
* defect: () => -1,
|
|
544
|
+
* });
|
|
540
545
|
* ```
|
|
541
546
|
*
|
|
542
547
|
* @category Guards
|
|
@@ -563,6 +568,30 @@ function passThrough(self) {
|
|
|
563
568
|
return self;
|
|
564
569
|
}
|
|
565
570
|
/**
|
|
571
|
+
* Adopt-and-silence a thenable a combinator is about to **discard**.
|
|
572
|
+
*
|
|
573
|
+
* @remarks
|
|
574
|
+
* The observers (`tap`, `tapErrCases`, `tapDefect`, `tapFailure`) throw their
|
|
575
|
+
* callback's return value away, and the `Result`-returning combinators reject a
|
|
576
|
+
* non-`Result` one. Either way, a thenable that slipped past `NotThenable` (a
|
|
577
|
+
* cast, a raw-JS caller) is dropped while still in flight — and if it later
|
|
578
|
+
* rejects, nothing is holding it, so the rejection floats unhandled and takes
|
|
579
|
+
* the process down on Node by default. Worse for an observer: its whole job is
|
|
580
|
+
* to make a failure visible, and this is the one path where the failure is
|
|
581
|
+
* invisible.
|
|
582
|
+
*
|
|
583
|
+
* Adopting it costs one microtask and makes the rejection a no-op. The
|
|
584
|
+
* boundaries already do exactly this for a thenable `qualify` and a thenable
|
|
585
|
+
* `fn` (see `interop.ts`); this is the same net on the combinator side.
|
|
586
|
+
*
|
|
587
|
+
* @internal
|
|
588
|
+
*/
|
|
589
|
+
function silenceIfThenable(value) {
|
|
590
|
+
try {
|
|
591
|
+
if ((typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function") Promise.resolve(value).then(void 0, () => void 0);
|
|
592
|
+
} catch {}
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
566
595
|
* The Defect minted when a callback constrained to return a `Result` returns
|
|
567
596
|
* something else — reachable only from untyped/cast callers (in typed code the
|
|
568
597
|
* constraint is a compile error). The combinator-side sibling of the
|
|
@@ -572,7 +601,8 @@ function passThrough(self) {
|
|
|
572
601
|
*
|
|
573
602
|
* @internal
|
|
574
603
|
*/
|
|
575
|
-
function nonResultCallbackDefect() {
|
|
604
|
+
function nonResultCallbackDefect(returned) {
|
|
605
|
+
silenceIfThenable(returned);
|
|
576
606
|
return defectRes(/* @__PURE__ */ new TypeError("unthrown: a combinator callback returned a non-Result value"));
|
|
577
607
|
}
|
|
578
608
|
/**
|
|
@@ -657,7 +687,7 @@ var AsyncRes = class AsyncRes {
|
|
|
657
687
|
if (r.tag !== "Ok") return passThrough(r);
|
|
658
688
|
try {
|
|
659
689
|
const inner = await f(r.value);
|
|
660
|
-
return isResult(inner) ? inner : nonResultCallbackDefect();
|
|
690
|
+
return isResult(inner) ? inner : nonResultCallbackDefect(inner);
|
|
661
691
|
} catch (cause) {
|
|
662
692
|
return defectRes(cause);
|
|
663
693
|
}
|
|
@@ -667,7 +697,7 @@ var AsyncRes = class AsyncRes {
|
|
|
667
697
|
return new AsyncRes(this.#promise.then((r) => {
|
|
668
698
|
if (r.tag !== "Ok") return r;
|
|
669
699
|
try {
|
|
670
|
-
f(r.value);
|
|
700
|
+
silenceIfThenable(f(r.value));
|
|
671
701
|
return r;
|
|
672
702
|
} catch (cause) {
|
|
673
703
|
return defectRes(cause);
|
|
@@ -679,7 +709,7 @@ var AsyncRes = class AsyncRes {
|
|
|
679
709
|
if (r.tag !== "Ok") return passThrough(r);
|
|
680
710
|
try {
|
|
681
711
|
const inner = await f(r.value);
|
|
682
|
-
if (!isResult(inner)) return nonResultCallbackDefect();
|
|
712
|
+
if (!isResult(inner)) return nonResultCallbackDefect(inner);
|
|
683
713
|
return inner.tag === "Ok" ? r : passThrough(inner);
|
|
684
714
|
} catch (cause) {
|
|
685
715
|
return defectRes(cause);
|
|
@@ -691,7 +721,7 @@ var AsyncRes = class AsyncRes {
|
|
|
691
721
|
if (r.tag !== "Ok") return passThrough(r);
|
|
692
722
|
try {
|
|
693
723
|
const inner = await f(r.value);
|
|
694
|
-
if (!isResult(inner)) return nonResultCallbackDefect();
|
|
724
|
+
if (!isResult(inner)) return nonResultCallbackDefect(inner);
|
|
695
725
|
if (inner.tag !== "Ok") return passThrough(inner);
|
|
696
726
|
return okRes({
|
|
697
727
|
...scopeOf(r.value),
|
|
@@ -750,7 +780,7 @@ var AsyncRes = class AsyncRes {
|
|
|
750
780
|
const out = runMatch(f, r.error);
|
|
751
781
|
if (isDefectMarker(out)) return defectRes(out.cause);
|
|
752
782
|
const inner = await out;
|
|
753
|
-
if (!isResult(inner)) return nonResultCallbackDefect();
|
|
783
|
+
if (!isResult(inner)) return nonResultCallbackDefect(inner);
|
|
754
784
|
return inner;
|
|
755
785
|
} catch (cause) {
|
|
756
786
|
return defectRes(cause);
|
|
@@ -775,6 +805,7 @@ var AsyncRes = class AsyncRes {
|
|
|
775
805
|
try {
|
|
776
806
|
const out = runMatch(f, r.error);
|
|
777
807
|
if (isDefectMarker(out)) return observerThrowToDefect(out.cause, r.error);
|
|
808
|
+
silenceIfThenable(out);
|
|
778
809
|
return r;
|
|
779
810
|
} catch (cause) {
|
|
780
811
|
return observerThrowToDefect(cause, r.error);
|
|
@@ -788,7 +819,7 @@ var AsyncRes = class AsyncRes {
|
|
|
788
819
|
const out = runMatch(f, r.error);
|
|
789
820
|
if (isDefectMarker(out)) return observerThrowToDefect(out.cause, r.error);
|
|
790
821
|
const inner = await out;
|
|
791
|
-
if (!isResult(inner)) return nonResultCallbackDefect();
|
|
822
|
+
if (!isResult(inner)) return nonResultCallbackDefect(inner);
|
|
792
823
|
return inner.tag === "Ok" ? passThrough(r) : passThrough(inner);
|
|
793
824
|
} catch (cause) {
|
|
794
825
|
return observerThrowToDefect(cause, r.error);
|
|
@@ -800,7 +831,7 @@ var AsyncRes = class AsyncRes {
|
|
|
800
831
|
if (r.tag !== "Defect") return r;
|
|
801
832
|
try {
|
|
802
833
|
const inner = await f(r.cause);
|
|
803
|
-
return isResult(inner) ? inner : nonResultCallbackDefect();
|
|
834
|
+
return isResult(inner) ? inner : nonResultCallbackDefect(inner);
|
|
804
835
|
} catch (cause) {
|
|
805
836
|
return defectRes(cause);
|
|
806
837
|
}
|
|
@@ -810,7 +841,7 @@ var AsyncRes = class AsyncRes {
|
|
|
810
841
|
return new AsyncRes(this.#promise.then((r) => {
|
|
811
842
|
if (r.tag !== "Defect") return r;
|
|
812
843
|
try {
|
|
813
|
-
f(r.cause);
|
|
844
|
+
silenceIfThenable(f(r.cause));
|
|
814
845
|
return r;
|
|
815
846
|
} catch (cause) {
|
|
816
847
|
return observerThrowToDefect(cause, r.cause);
|
|
@@ -821,7 +852,7 @@ var AsyncRes = class AsyncRes {
|
|
|
821
852
|
return new AsyncRes(this.#promise.then((r) => {
|
|
822
853
|
if (r.tag === "Ok") return r;
|
|
823
854
|
try {
|
|
824
|
-
f(r);
|
|
855
|
+
silenceIfThenable(f(r));
|
|
825
856
|
return r;
|
|
826
857
|
} catch (cause) {
|
|
827
858
|
return observerThrowToDefect(cause, r.tag === "Err" ? r.error : r.cause);
|
|
@@ -1089,6 +1120,11 @@ function fromNullable(value, onAbsent) {
|
|
|
1089
1120
|
* — and a thenable slipped past the types at runtime becomes a `Defect` (never
|
|
1090
1121
|
* an `Err(Promise)`), its orphaned rejection silenced.
|
|
1091
1122
|
*
|
|
1123
|
+
* `fn` is **synchronous** too. An `async` `fn` rejects *after* this boundary has
|
|
1124
|
+
* already returned, so its rejection could never reach `qualify`: it becomes a
|
|
1125
|
+
* `Defect` (never `Ok(<Promise>)`) and the orphaned rejection is silenced rather
|
|
1126
|
+
* than left to float. Reach for {@link fromPromise} to wrap async work.
|
|
1127
|
+
*
|
|
1092
1128
|
* The modeled error type is `Exclude<R, Defect>` — the `Defect` arm of
|
|
1093
1129
|
* `qualify`'s return is **subtracted** from `E`, never inferred into it. So a
|
|
1094
1130
|
* `qualify` that returns *only* `defect(cause)` yields `E = never` (a Defect is
|
|
@@ -1125,7 +1161,8 @@ function fromThrowable(fn, qualify) {
|
|
|
1125
1161
|
const triage = qualify;
|
|
1126
1162
|
return (...args) => {
|
|
1127
1163
|
try {
|
|
1128
|
-
|
|
1164
|
+
const value = fn(...args);
|
|
1165
|
+
return isThenable(value) ? thenableReturnDefect(value) : Ok(value);
|
|
1129
1166
|
} catch (cause) {
|
|
1130
1167
|
return qualifyToResult(cause, triage);
|
|
1131
1168
|
}
|
|
@@ -1142,6 +1179,10 @@ function fromThrowable(fn, qualify) {
|
|
|
1142
1179
|
* `qualify`. When some throws *are* anticipated, reach for
|
|
1143
1180
|
* {@link fromThrowable} and triage them.
|
|
1144
1181
|
*
|
|
1182
|
+
* `fn` is **synchronous**: an `async` `fn` becomes a `Defect` (never
|
|
1183
|
+
* `Ok(<Promise>)`), with its orphaned rejection silenced rather than left to
|
|
1184
|
+
* float. Reach for {@link fromSafePromise} to wrap async work.
|
|
1185
|
+
*
|
|
1145
1186
|
* @typeParam A - the wrapped function's argument tuple.
|
|
1146
1187
|
* @typeParam T - the wrapped function's return type.
|
|
1147
1188
|
* @param fn - the throwing function to wrap.
|
|
@@ -1163,7 +1204,8 @@ function fromThrowable(fn, qualify) {
|
|
|
1163
1204
|
function fromSafeThrowable(fn) {
|
|
1164
1205
|
return (...args) => {
|
|
1165
1206
|
try {
|
|
1166
|
-
|
|
1207
|
+
const value = fn(...args);
|
|
1208
|
+
return isThenable(value) ? thenableReturnDefect(value) : Ok(value);
|
|
1167
1209
|
} catch (cause) {
|
|
1168
1210
|
return defectRes(cause);
|
|
1169
1211
|
}
|
|
@@ -1259,7 +1301,33 @@ function qualifyToResult(cause, qualify) {
|
|
|
1259
1301
|
}
|
|
1260
1302
|
}
|
|
1261
1303
|
/**
|
|
1262
|
-
*
|
|
1304
|
+
* The Defect minted when a **synchronous** boundary's `fn` returns a thenable —
|
|
1305
|
+
* i.e. an `async` function was handed to {@link fromThrowable} /
|
|
1306
|
+
* {@link fromSafeThrowable}.
|
|
1307
|
+
*
|
|
1308
|
+
* @remarks
|
|
1309
|
+
* This is the sibling of the thenable-`qualify` net in {@link qualifyToResult},
|
|
1310
|
+
* and it closes a strictly worse hole. A synchronous boundary only ever sees a
|
|
1311
|
+
* synchronous `throw`, so an async `fn`'s rejection never reaches `qualify` at
|
|
1312
|
+
* all: it would sit inside `Ok(<Promise>)`, un-triaged, and then float as an
|
|
1313
|
+
* unhandled rejection — which terminates the process on Node by default.
|
|
1314
|
+
*
|
|
1315
|
+
* Unlike the combinator callbacks, this cannot be banned at compile time
|
|
1316
|
+
* without collateral damage: `T & NotThenable<T>` on `fn`'s return makes a
|
|
1317
|
+
* **generic** function unassignable, so `fromSafeThrowable(structuredClone)`
|
|
1318
|
+
* stops compiling and `T` collapses to `unknown`. (The phantom rest-tuple guard
|
|
1319
|
+
* `fromPromise` uses fares worse.) So the ban is enforced here, at runtime,
|
|
1320
|
+
* where it costs nothing: a Defect, plus adopt-and-silence so the orphaned
|
|
1321
|
+
* rejection cannot float.
|
|
1322
|
+
*
|
|
1323
|
+
* @internal
|
|
1324
|
+
*/
|
|
1325
|
+
function thenableReturnDefect(value) {
|
|
1326
|
+
Promise.resolve(value).then(void 0, () => void 0);
|
|
1327
|
+
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."));
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Runtime thenable probe for the belt-and-braces guards above. Called inside the
|
|
1263
1331
|
* caller's `try`, so even a hostile `.then` getter lands on the Defect path.
|
|
1264
1332
|
*
|
|
1265
1333
|
* @internal
|
|
@@ -1387,7 +1455,10 @@ function allFromDict(results) {
|
|
|
1387
1455
|
* ```ts
|
|
1388
1456
|
* import { allAsync, fromSafePromise } from "unthrown";
|
|
1389
1457
|
*
|
|
1390
|
-
* const both = allAsync([
|
|
1458
|
+
* const both = allAsync([
|
|
1459
|
+
* fromSafePromise(Promise.resolve(1)),
|
|
1460
|
+
* fromSafePromise(Promise.resolve(2)),
|
|
1461
|
+
* ]);
|
|
1391
1462
|
* (await both).get(); // => [1, 2]
|
|
1392
1463
|
* ```
|
|
1393
1464
|
*/
|
|
@@ -1491,7 +1562,10 @@ const Result = {
|
|
|
1491
1562
|
* @example
|
|
1492
1563
|
* ```ts
|
|
1493
1564
|
* import { AsyncResult } from "unthrown";
|
|
1494
|
-
* const user = await AsyncResult.fromPromise(
|
|
1565
|
+
* const user = await AsyncResult.fromPromise(
|
|
1566
|
+
* fetchUser(id),
|
|
1567
|
+
* (c, defect) => defect(c),
|
|
1568
|
+
* );
|
|
1495
1569
|
* user.get(); // => the fetched user (on success)
|
|
1496
1570
|
* ```
|
|
1497
1571
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unthrown",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "Explicit errors as values, with a separate defect (panic) channel",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"defect",
|
|
@@ -47,13 +47,10 @@
|
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@btravstack/tsconfig": "0.2.0",
|
|
50
|
-
"@
|
|
51
|
-
"@types/node": "26.1.1",
|
|
50
|
+
"@types/node": "26.1.2",
|
|
52
51
|
"@vitest/coverage-v8": "4.1.10",
|
|
53
|
-
"tsdown": "0.22.
|
|
54
|
-
"
|
|
55
|
-
"typedoc-plugin-markdown": "4.12.0",
|
|
56
|
-
"typescript": "6.0.3",
|
|
52
|
+
"tsdown": "0.22.14",
|
|
53
|
+
"typescript": "7.0.2",
|
|
57
54
|
"vitest": "4.1.10"
|
|
58
55
|
},
|
|
59
56
|
"engines": {
|
|
@@ -61,7 +58,6 @@
|
|
|
61
58
|
},
|
|
62
59
|
"scripts": {
|
|
63
60
|
"build": "tsdown src/index.ts --format cjs,esm --dts --clean",
|
|
64
|
-
"build:docs": "typedoc",
|
|
65
61
|
"dev": "tsdown src/index.ts --format cjs,esm --dts --watch",
|
|
66
62
|
"test": "vitest run",
|
|
67
63
|
"test:types": "tsc --noEmit -p tsconfig.test-d.json",
|