@nlozgachev/pipelined 0.63.0 → 0.64.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/core.mjs CHANGED
@@ -2108,22 +2108,120 @@ function interpretFn(op, options) {
2108
2108
  }
2109
2109
  }
2110
2110
  var Op = {
2111
- nil: makeNil,
2111
+ make: {
2112
+ /**
2113
+ * Creates an Ok outcome with the given value.
2114
+ *
2115
+ * @example
2116
+ * ```ts
2117
+ * Op.make.ok(42); // { kind: "OpOk", value: 42 }
2118
+ * ```
2119
+ */
2120
+ ok: makeOk,
2121
+ /**
2122
+ * Creates an Err outcome with the given error.
2123
+ *
2124
+ * @example
2125
+ * ```ts
2126
+ * Op.make.err("Something went wrong"); // { kind: "OpErr", error: "Something went wrong" }
2127
+ * ```
2128
+ */
2129
+ err: makeErr,
2130
+ /**
2131
+ * Creates a Nil outcome with the given cancellation/drop reason.
2132
+ *
2133
+ * @example
2134
+ * ```ts
2135
+ * Op.make.nil("aborted"); // { kind: "OpNil", reason: "aborted" }
2136
+ * ```
2137
+ */
2138
+ nil: makeNil
2139
+ },
2140
+ is: {
2141
+ /**
2142
+ * Type guard that checks if an Op state is Idle.
2143
+ *
2144
+ * @example
2145
+ * ```ts
2146
+ * if (Op.is.idle(manager.state)) {
2147
+ * console.log("Ready to execute");
2148
+ * }
2149
+ * ```
2150
+ */
2151
+ idle: isIdle,
2152
+ /**
2153
+ * Type guard that checks if an Op state is Pending (actively executing).
2154
+ *
2155
+ * @example
2156
+ * ```ts
2157
+ * if (Op.is.pending(manager.state)) {
2158
+ * showSpinner();
2159
+ * }
2160
+ * ```
2161
+ */
2162
+ pending: isPending,
2163
+ /**
2164
+ * Type guard that checks if an Op state is Queued (waiting in a concurrency queue).
2165
+ *
2166
+ * @example
2167
+ * ```ts
2168
+ * if (Op.is.queued(manager.state)) {
2169
+ * console.log("Position in queue:", manager.state.position);
2170
+ * }
2171
+ * ```
2172
+ */
2173
+ queued: isQueued,
2174
+ /**
2175
+ * Type guard that checks if an Op state is Retrying after a failure.
2176
+ *
2177
+ * @example
2178
+ * ```ts
2179
+ * if (Op.is.retrying(manager.state)) {
2180
+ * console.log("Retry attempt:", manager.state.attempt);
2181
+ * }
2182
+ * ```
2183
+ */
2184
+ retrying: isRetrying,
2185
+ /**
2186
+ * Type guard that checks if an Op state or outcome is Ok.
2187
+ *
2188
+ * @example
2189
+ * ```ts
2190
+ * if (Op.is.ok(outcome)) {
2191
+ * render(outcome.value);
2192
+ * }
2193
+ * ```
2194
+ */
2195
+ ok: isOk,
2196
+ /**
2197
+ * Type guard that checks if an Op state or outcome is Err.
2198
+ *
2199
+ * @example
2200
+ * ```ts
2201
+ * if (Op.is.err(outcome)) {
2202
+ * showError(outcome.error);
2203
+ * }
2204
+ * ```
2205
+ */
2206
+ err: isErr,
2207
+ /**
2208
+ * Type guard that checks if an Op state or outcome is Nil.
2209
+ *
2210
+ * @example
2211
+ * ```ts
2212
+ * if (Op.is.nil(outcome)) {
2213
+ * console.log("Skipped due to:", outcome.reason);
2214
+ * }
2215
+ * ```
2216
+ */
2217
+ nil: isNil
2218
+ },
2112
2219
  create: (factory, onError) => ({
2113
2220
  _factory: (input, signal) => Deferred.from.Promise(
2114
2221
  factory(signal)(input).then((value) => Result.make.ok(value)).catch((error) => signal.aborted ? null : Result.make.err(onError(error)))
2115
2222
  )
2116
2223
  }),
2117
2224
  lift: (f) => Op.create((signal) => (input) => f(input, signal), (e) => e),
2118
- ok: makeOk,
2119
- err: makeErr,
2120
- isIdle,
2121
- isPending,
2122
- isQueued,
2123
- isRetrying,
2124
- isOk,
2125
- isErr,
2126
- isNil,
2127
2225
  match: (cases) => (outcome) => {
2128
2226
  if (outcome.kind === "OpOk") {
2129
2227
  return cases.ok(outcome.value);
@@ -4279,9 +4377,9 @@ var Stream = {
4279
4377
  var makeSome2 = (value) => Task.resolve(Maybe.make.some(value));
4280
4378
  var makeNone2 = () => Task.resolve(Maybe.make.none());
4281
4379
  var mapTaskMaybe = (f) => (data) => Task.map(Maybe.map(f))(data);
4282
- var chainTaskMaybe = (f) => (data) => Task.chain(
4283
- (option) => Maybe.is.some(option) ? f(option.value) : Task.resolve(Maybe.make.none())
4284
- )(data);
4380
+ var chainTaskMaybe = (f) => (data) => Task.chain((option) => Maybe.is.some(option) ? f(option.value) : Task.resolve(Maybe.make.none()))(
4381
+ data
4382
+ );
4285
4383
  var TaskMaybe = {
4286
4384
  /**
4287
4385
  * Wraps a value in a Some inside a Task.
@@ -4314,8 +4412,6 @@ var TaskMaybe = {
4314
4412
  */
4315
4413
  none: makeNone2
4316
4414
  },
4317
- some: makeSome2,
4318
- none: makeNone2,
4319
4415
  // --- from ---
4320
4416
  from: {
4321
4417
  /**
@@ -4451,7 +4547,7 @@ var TaskMaybe = {
4451
4547
  *
4452
4548
  * @example
4453
4549
  * ```ts
4454
- * pipe(Task.Maybe.some(42), Task.Maybe.bindTo("value")); // Task.Maybe({ value: 42 })
4550
+ * pipe(Task.Maybe.make.some(42), Task.Maybe.bindTo("value")); // Task.Maybe({ value: 42 })
4455
4551
  * ```
4456
4552
  */
4457
4553
  bindTo: (key) => (data) => mapTaskMaybe((a) => ({ [key]: a }))(data),
@@ -4461,8 +4557,8 @@ var TaskMaybe = {
4461
4557
  * @example
4462
4558
  * ```ts
4463
4559
  * pipe(
4464
- * Task.Maybe.some({ a: 1 }),
4465
- * Task.Maybe.bind("b", ({ a }) => Task.Maybe.some(a + 1))
4560
+ * Task.Maybe.make.some({ a: 1 }),
4561
+ * Task.Maybe.bind("b", ({ a }) => Task.Maybe.make.some(a + 1))
4466
4562
  * ); // Task.Maybe({ a: 1, b: 2 })
4467
4563
  * ```
4468
4564
  */
@@ -4475,14 +4571,12 @@ var TaskMaybe = {
4475
4571
  * @example
4476
4572
  * ```ts
4477
4573
  * pipe(
4478
- * Task.Maybe.none(),
4479
- * Task.Maybe.recover(() => Task.Maybe.some(42))
4574
+ * Task.Maybe.make.none(),
4575
+ * Task.Maybe.recover(() => Task.Maybe.make.some(42))
4480
4576
  * ); // Task.Maybe(42)
4481
4577
  * ```
4482
4578
  */
4483
- recover: (fallback) => (data) => Task.chain((maybe) => Maybe.is.none(maybe) ? fallback() : Task.resolve(maybe))(
4484
- data
4485
- ),
4579
+ recover: (fallback) => (data) => Task.chain((maybe) => Maybe.is.none(maybe) ? fallback() : Task.resolve(maybe))(data),
4486
4580
  /**
4487
4581
  * Combines a record of Task.Maybes into a single Task.Maybe of a record.
4488
4582
  * Evaluates fields in parallel and returns None if any task resolves to None.
@@ -4490,8 +4584,8 @@ var TaskMaybe = {
4490
4584
  * @example
4491
4585
  * ```ts
4492
4586
  * Task.Maybe.struct({
4493
- * name: Task.Maybe.some("Alice"),
4494
- * age: Task.Maybe.some(30)
4587
+ * name: Task.Maybe.make.some("Alice"),
4588
+ * age: Task.Maybe.make.some(30)
4495
4589
  * }); // Task.Maybe({ name: "Alice", age: 30 })
4496
4590
  * ```
4497
4591
  */
@@ -4552,8 +4646,6 @@ var TaskResult = {
4552
4646
  */
4553
4647
  err: makeErr3
4554
4648
  },
4555
- ok: makeOk3,
4556
- err: makeErr3,
4557
4649
  // --- from ---
4558
4650
  from: {
4559
4651
  /**
@@ -4595,7 +4687,7 @@ var TaskResult = {
4595
4687
  *
4596
4688
  * @example
4597
4689
  * ```ts
4598
- * const taskResult = Task.Result.ok(42);
4690
+ * const taskResult = Task.Result.make.ok(42);
4599
4691
  * const taskMaybe = pipe(taskResult, Task.Result.to.Maybe);
4600
4692
  * ```
4601
4693
  */
@@ -4658,7 +4750,7 @@ var TaskResult = {
4658
4750
  * fetchTask,
4659
4751
  * Task.Result.recoverUnless(
4660
4752
  * (e) => e === "fatal",
4661
- * () => Task.Result.ok("fallback")
4753
+ * () => Task.Result.make.ok("fallback")
4662
4754
  * )
4663
4755
  * );
4664
4756
  * ```
@@ -4721,7 +4813,7 @@ var TaskResult = {
4721
4813
  *
4722
4814
  * @example
4723
4815
  * ```ts
4724
- * pipe(Task.Result.ok(42), Task.Result.bindTo("value")); // Task.Result({ value: 42 })
4816
+ * pipe(Task.Result.make.ok(42), Task.Result.bindTo("value")); // Task.Result({ value: 42 })
4725
4817
  * ```
4726
4818
  */
4727
4819
  bindTo: (key) => (data) => mapTaskResult((a) => ({ [key]: a }))(data),
@@ -4731,8 +4823,8 @@ var TaskResult = {
4731
4823
  * @example
4732
4824
  * ```ts
4733
4825
  * pipe(
4734
- * Task.Result.ok({ a: 1 }),
4735
- * Task.Result.bind("b", ({ a }) => Task.Result.ok(a + 1))
4826
+ * Task.Result.make.ok({ a: 1 }),
4827
+ * Task.Result.bind("b", ({ a }) => Task.Result.make.ok(a + 1))
4736
4828
  * ); // Task.Result({ a: 1, b: 2 })
4737
4829
  * ```
4738
4830
  */
@@ -4747,8 +4839,8 @@ var TaskResult = {
4747
4839
  * @example
4748
4840
  * ```ts
4749
4841
  * Task.Result.struct({
4750
- * name: Task.Result.ok("Alice"),
4751
- * age: Task.Result.ok(30)
4842
+ * name: Task.Result.make.ok("Alice"),
4843
+ * age: Task.Result.make.ok(30)
4752
4844
  * }); // Task.Result({ name: "Alice", age: 30 })
4753
4845
  * ```
4754
4846
  */
@@ -4868,1553 +4960,1550 @@ var TaskResult = {
4868
4960
  // src/internal/InternalTypes.ts
4869
4961
  var isNonEmptyArr = (list) => list.length > 0;
4870
4962
 
4871
- // src/Core/TaskValidation.ts
4872
- var makePassed = (value) => Task.resolve(Validation.make.passed(value));
4873
- var makeFailed = (error) => Task.resolve(Validation.make.failed(error));
4874
- var makeFailedAll = (errors) => Task.resolve(Validation.make.failedAll(errors));
4875
- var TaskValidation = {
4963
+ // src/Core/Validation.ts
4964
+ var makePassed = (value) => ({ kind: "Passed", value });
4965
+ var makeFailed = (error) => ({ kind: "Failed", errors: [error] });
4966
+ var makeFailedAll = (errors) => ({ kind: "Failed", errors });
4967
+ var isPassed = (data) => data.kind === "Passed";
4968
+ var isFailed = (data) => data.kind === "Failed";
4969
+ function toResult(arg) {
4970
+ if (typeof arg === "function") {
4971
+ const combine = arg;
4972
+ return (val) => isPassed(val) ? Result.make.ok(val.value) : Result.make.err(combine(val.errors));
4973
+ }
4974
+ return isPassed(arg) ? Result.make.ok(arg.value) : Result.make.err(arg.errors);
4975
+ }
4976
+ var Validation = {
4876
4977
  make: {
4877
4978
  /**
4878
- * Wraps a value in a passed Task.Validation.
4979
+ * Wraps a value in a passed Validation.
4879
4980
  *
4880
4981
  * @example
4881
4982
  * ```ts
4882
- * const task = Task.Validation.make.passed(42);
4883
- * const res = await task(); // Passed(42)
4983
+ * Validation.make.passed(42); // Passed(42)
4884
4984
  * ```
4885
4985
  */
4886
4986
  passed: makePassed,
4887
4987
  /**
4888
- * Creates a failed Task.Validation with a single error.
4988
+ * Creates a failed Validation from a single error.
4889
4989
  *
4890
4990
  * @example
4891
4991
  * ```ts
4892
- * const task = Task.Validation.make.failed("invalid");
4893
- * const res = await task(); // Failed(["invalid"])
4992
+ * Validation.make.failed("Invalid input");
4894
4993
  * ```
4895
4994
  */
4896
4995
  failed: makeFailed,
4897
4996
  /**
4898
- * Creates a failed Task.Validation from multiple errors.
4997
+ * Creates a failed Validation from multiple errors.
4899
4998
  *
4900
4999
  * @example
4901
5000
  * ```ts
4902
- * const task = Task.Validation.make.failedAll(["err1", "err2"]);
4903
- * const res = await task(); // Failed(["err1", "err2"])
5001
+ * Validation.make.failedAll(["Invalid input"]);
4904
5002
  * ```
4905
5003
  */
4906
5004
  failedAll: makeFailedAll
4907
5005
  },
4908
- passed: makePassed,
4909
- failed: makeFailed,
4910
- failedAll: makeFailedAll,
4911
- // --- from ---
4912
- from: {
5006
+ is: {
4913
5007
  /**
4914
- * Lifts a Validation into a Task.Validation.
5008
+ * Type guard that checks if a Validation is passed.
4915
5009
  *
4916
5010
  * @example
4917
5011
  * ```ts
4918
- * Task.Validation.from.Validation(Validation.make.passed(42));
5012
+ * const v = Validation.make.passed(42);
5013
+ * if (Validation.is.passed(v)) {
5014
+ * console.log(v.value); // 42
5015
+ * }
4919
5016
  * ```
4920
5017
  */
4921
- Validation: (validation) => Task.resolve(validation),
5018
+ passed: isPassed,
4922
5019
  /**
4923
- * Creates a Task.Validation from a nullable value.
4924
- * If the value is null or undefined, returns Failed with the error from onNull.
4925
- * Otherwise, returns Passed.
5020
+ * Type guard that checks if a Validation is failed.
4926
5021
  *
4927
5022
  * @example
4928
5023
  * ```ts
4929
- * Task.Validation.from.nullable(() => "missing")(42); // resolves to Passed(42)
4930
- * Task.Validation.from.nullable(() => "missing")(null); // resolves to Failed(["missing"])
5024
+ * const v = Validation.make.failed("invalid");
5025
+ * if (Validation.is.failed(v)) {
5026
+ * console.log(v.errors); // ["invalid"]
5027
+ * }
4931
5028
  * ```
4932
5029
  */
4933
- nullable: (onNull) => (value) => Task.resolve(
4934
- value === null || value === void 0 ? Validation.make.failed(onNull()) : Validation.make.passed(value)
4935
- ),
5030
+ failed: isFailed
5031
+ },
5032
+ /**
5033
+ * Creates a Validation from a synchronous thunk that may throw.
5034
+ * Catches any errors and transforms them using the `onError` function into a Failed validation.
5035
+ *
5036
+ * @example
5037
+ * ```ts
5038
+ * const result = Validation.tryCatch(
5039
+ * () => JSON.parse(rawString),
5040
+ * { onError: (e) => `Parse error: ${e}` }
5041
+ * );
5042
+ * ```
5043
+ */
5044
+ tryCatch: (f, options) => {
5045
+ try {
5046
+ return makePassed(f());
5047
+ } catch (error) {
5048
+ return makeFailed(options.onError(error));
5049
+ }
5050
+ },
5051
+ // --- from ---
5052
+ from: {
4936
5053
  /**
4937
- * Creates a Task.Validation from a Maybe.
4938
- * Some becomes Passed, None becomes Failed with the error from onNone.
5054
+ * Creates a Validation from a predicate applied to a value.
5055
+ * Returns Passed if the predicate passes, Failed from `onFalse` otherwise.
4939
5056
  *
4940
5057
  * @example
4941
5058
  * ```ts
4942
- * Task.Validation.from.Maybe(() => "empty")(Maybe.make.some(42)); // resolves to Passed(42)
4943
- * Task.Validation.from.Maybe(() => "empty")(Maybe.make.none()); // resolves to Failed(["empty"])
5059
+ * const validateName = Validation.from.Predicate(
5060
+ * (s: string) => s.length > 0,
5061
+ * () => "Name is required"
5062
+ * );
5063
+ *
5064
+ * validateName("Alice"); // Passed("Alice")
5065
+ * validateName(""); // Failed(["Name is required"])
4944
5066
  * ```
4945
5067
  */
4946
- Maybe: (onNone) => (maybe) => Task.resolve(
4947
- Maybe.is.none(maybe) ? Validation.make.failed(onNone()) : Validation.make.passed(maybe.value)
4948
- ),
5068
+ Predicate: (pred, onFalse) => (a) => pred(a) ? makePassed(a) : makeFailed(onFalse(a)),
4949
5069
  /**
4950
- * Creates a Task.Validation from a Result.
4951
- * Ok becomes Passed, Err(e) becomes Failed([e]).
5070
+ * Creates a Validation from a nullable value.
5071
+ * If the value is null or undefined, returns Failed with the error from onNull.
5072
+ * Otherwise, returns Passed.
4952
5073
  *
4953
5074
  * @example
4954
5075
  * ```ts
4955
- * Task.Validation.from.Result(Result.make.ok(42)); // resolves to Passed(42)
4956
- * Task.Validation.from.Result(Result.make.err("bad")); // resolves to Failed(["bad"])
5076
+ * pipe(null, Validation.from.nullable(() => "is null")); // Failed(["is null"])
5077
+ * pipe(42, Validation.from.nullable(() => "is null")); // Passed(42)
4957
5078
  * ```
4958
5079
  */
4959
- Result: (result) => Task.resolve(Validation.from.Result(result))
4960
- },
4961
- // --- to ---
4962
- to: {
5080
+ nullable: (onNull) => (value) => value === null || value === void 0 ? makeFailed(onNull()) : makePassed(value),
4963
5081
  /**
4964
- * Converts a `Task.Validation` to a `Task.Result`, combining accumulated errors using `combineErrors`.
4965
- * `Passed(a)` becomes `Ok(a)`; `Failed(errors)` becomes `Err(combineErrors(errors))`.
5082
+ * Creates a Validation from a Maybe.
5083
+ * If the Maybe is None, returns Failed with the error from onNone.
5084
+ * Otherwise, returns Passed.
4966
5085
  *
4967
5086
  * @example
4968
5087
  * ```ts
4969
- * Task.Validation.to.Result((errors) => errors.join(", "))(validationTask);
5088
+ * pipe(Maybe.make.none(), Validation.from.Maybe(() => "is none")); // Failed(["is none"])
5089
+ * pipe(Maybe.make.some(42), Validation.from.Maybe(() => "is none")); // Passed(42)
4970
5090
  * ```
4971
5091
  */
4972
- Result: (combineErrors) => (data) => Task.map(Validation.to.Result(combineErrors))(data),
5092
+ Maybe: (onNone) => (maybe) => Maybe.is.none(maybe) ? makeFailed(onNone()) : makePassed(maybe.value),
4973
5093
  /**
4974
- * Converts a `Task.Validation` to a `Task.Maybe`.
4975
- * `Passed(a)` becomes `Some(a)`; `Failed(errors)` becomes `None` (errors are discarded).
5094
+ * Converts a `Result` to a `Validation`. `Ok` becomes `Passed`; `Err(e)` becomes `Failed([e])`.
5095
+ *
5096
+ * Useful when bridging from error-short-circuiting `Result` pipelines into
5097
+ * error-accumulating `Validation` pipelines.
4976
5098
  *
4977
5099
  * @example
4978
5100
  * ```ts
4979
- * Task.Validation.to.Maybe(validationTask);
5101
+ * Validation.from.Result(Result.make.ok(42)); // Passed(42)
5102
+ * Validation.from.Result(Result.make.err("bad")); // Failed(["bad"])
4980
5103
  * ```
4981
5104
  */
4982
- Maybe: (data) => Task.map(Validation.to.Maybe)(data)
5105
+ Result: (data) => data.kind === "Ok" ? makePassed(data.value) : makeFailed(data.error)
4983
5106
  },
4984
5107
  /**
4985
- * Creates a Task.Validation from a Promise-returning thunk that may throw or reject.
4986
- * Catches any errors and transforms them using the `onError` function into a Failed validation.
4987
- * The thunk optionally receives an `AbortSignal` forwarded from the call site.
5108
+ * Transforms the success value inside a Validation.
4988
5109
  *
4989
5110
  * @example
4990
5111
  * ```ts
4991
- * const loadConfig = Task.Validation.tryCatch(
4992
- * (signal) => configStore.get("default", { signal }),
4993
- * { onError: (e) => `Failed to load config: ${e}` }
4994
- * );
5112
+ * pipe(Validation.make.passed(5), Validation.map(n => n * 2)); // Passed(10)
5113
+ * pipe(Validation.make.failed("oops"), Validation.map(n => n * 2)); // Failed(["oops"])
4995
5114
  * ```
4996
5115
  */
4997
- tryCatch: (f, options) => (signal) => Deferred.from.Promise(
4998
- // oxlint-disable-next-line require-await
4999
- globalThis.Promise.resolve().then(async () => f(signal)).then(Validation.make.passed).catch(
5000
- (error) => Validation.make.failed(options.onError(error))
5001
- )
5002
- ),
5003
- /**
5004
- * Transforms the success value inside a Task.Validation.
5005
- */
5006
- map: (f) => (data) => Task.map(Validation.map(f))(data),
5116
+ map: (f) => (data) => isPassed(data) ? makePassed(f(data.value)) : data,
5007
5117
  /**
5008
- * Applies a function wrapped in a Task.Validation to a value wrapped in a
5009
- * Task.Validation. Both Tasks run in parallel and errors from both sides
5010
- * are accumulated.
5118
+ * Transforms the error list inside a Validation.
5011
5119
  *
5012
5120
  * @example
5013
5121
  * ```ts
5014
- * pipe(
5015
- * Task.Validation.passed((name: string) => (age: number) => ({ name, age })),
5016
- * Task.Validation.ap(validateName(name)),
5017
- * Task.Validation.ap(validateAge(age))
5018
- * )();
5122
+ * pipe(Validation.make.failed("oops"), Validation.mapError(e => e.toUpperCase())); // Failed(["OOPS"])
5019
5123
  * ```
5020
5124
  */
5021
- ap: (arg) => (data) => (signal) => Deferred.from.Promise(
5022
- Promise.all([Deferred.to.Promise(data(signal)), Deferred.to.Promise(arg(signal))]).then(
5023
- ([vf, va]) => Validation.ap(va)(vf)
5024
- )
5025
- ),
5026
- /**
5027
- * Extracts a value from a Task.Validation by providing handlers for both cases.
5028
- */
5029
- fold: (onFailed, onPassed) => (data) => Task.map(Validation.fold(onFailed, onPassed))(data),
5125
+ mapError: (f) => (data) => isFailed(data) ? makeFailedAll(data.errors.map(f)) : data,
5030
5126
  /**
5031
- * Pattern matches on a Task.Validation, returning a Task of the result.
5127
+ * Applies a function wrapped in a Validation to a value wrapped in a Validation.
5128
+ * Accumulates errors from both sides.
5032
5129
  *
5033
5130
  * @example
5034
5131
  * ```ts
5132
+ * const add = (a: number) => (b: number) => a + b;
5035
5133
  * pipe(
5036
- * validateForm(input),
5037
- * Task.Validation.match({
5038
- * passed: data => save(data),
5039
- * failed: errors => showErrors(errors)
5040
- * })
5041
- * )();
5134
+ * Validation.make.passed(add),
5135
+ * Validation.ap(Validation.make.passed(5)),
5136
+ * Validation.ap(Validation.make.passed(3))
5137
+ * ); // Passed(8)
5138
+ *
5139
+ * pipe(
5140
+ * Validation.make.passed(add),
5141
+ * Validation.ap(Validation.make.failed<string>("bad a")),
5142
+ * Validation.ap(Validation.make.failed<string>("bad b"))
5143
+ * ); // Failed(["bad a", "bad b"])
5042
5144
  * ```
5043
5145
  */
5044
- match: (cases) => (data) => Task.map(Validation.match(cases))(data),
5045
- /**
5046
- * Returns the success value or a default value if the Task.Validation is failed.
5047
- * The default can be a different type, widening the result to `Task<A | B>`.
5048
- */
5049
- getOrElse: (defaultValue) => (data) => Task.map(Validation.getOrElse(defaultValue))(data),
5050
- /**
5051
- * Executes a side effect on the success value without changing the Task.Validation.
5052
- * Useful for logging or debugging.
5053
- */
5054
- tap: (f) => (data) => Task.map(Validation.tap(f))(data),
5055
- /**
5056
- * Recovers from a Failed state by providing a fallback Task.Validation.
5057
- * The fallback receives the accumulated error list so callers can inspect which errors occurred.
5058
- * The fallback can produce a different success type, widening the result to `Task.Validation<E, A | B>`.
5059
- */
5060
- recover: (fallback) => (data) => Task.chain(
5061
- (validation) => Validation.is.passed(validation) ? Task.resolve(validation) : fallback(validation.errors)
5062
- )(data),
5146
+ ap: (arg) => (data) => {
5147
+ if (isPassed(data)) {
5148
+ return isPassed(arg) ? makePassed(data.value(arg.value)) : makeFailedAll(arg.errors);
5149
+ }
5150
+ return isPassed(arg) ? makeFailedAll(data.errors) : makeFailedAll([...data.errors, ...arg.errors]);
5151
+ },
5063
5152
  /**
5064
- * Recovers from a Failed state unless the predicate `isBlocked` returns true for the accumulated errors.
5065
- * The fallback receives the accumulated errors and can produce a different success type, widening the result to `Task.Validation<E, A | B>`.
5153
+ * Applies a function wrapped in a Validation to a value wrapped in a Validation,
5154
+ * using a custom error concatenator function when both sides fail.
5066
5155
  *
5067
5156
  * @example
5068
5157
  * ```ts
5069
- * pipe(
5070
- * validationTask,
5071
- * Task.Validation.recoverUnless(
5072
- * (errors) => errors.includes("fatal"),
5073
- * (errors) => Task.Validation.passed("fallback")
5074
- * )
5075
- * );
5158
+ * const concat = (e1: NonEmptyArr<string>, e2: NonEmptyArr<string>): NonEmptyArr<string> =>
5159
+ * [...e1, ...e2];
5160
+ * pipe(fnVal, Validation.apCustom(concat)(argVal));
5076
5161
  * ```
5077
5162
  */
5078
- recoverUnless: (isBlocked, fallback) => (data) => Task.chain(
5079
- (validation) => Validation.is.passed(validation) ? Task.resolve(validation) : isBlocked(validation.errors) ? Task.resolve(validation) : fallback(validation.errors)
5080
- )(data),
5163
+ apCustom: (concat) => (arg) => (data) => {
5164
+ if (isPassed(data)) {
5165
+ return isPassed(arg) ? makePassed(data.value(arg.value)) : makeFailedAll(arg.errors);
5166
+ }
5167
+ return isPassed(arg) ? makeFailedAll(data.errors) : makeFailedAll(concat(data.errors, arg.errors));
5168
+ },
5081
5169
  /**
5082
- * Runs two Task.Validations concurrently and combines their results into a tuple.
5083
- * If both are Passed, returns Passed with both values. If either fails, accumulates
5084
- * errors from both sides.
5170
+ * Extracts the value from a Validation by providing handlers for both cases.
5085
5171
  *
5086
5172
  * @example
5087
5173
  * ```ts
5088
- * await Task.Validation.product(
5089
- * validateName(form.name),
5090
- * validateAge(form.age),
5091
- * )(); // Passed(["Alice", 30]) or Failed([...errors])
5174
+ * pipe(
5175
+ * Validation.make.passed(42),
5176
+ * Validation.fold(
5177
+ * errors => `Errors: ${errors.join(", ")}`,
5178
+ * value => `Value: ${value}`
5179
+ * )
5180
+ * );
5092
5181
  * ```
5093
5182
  */
5094
- product: (first, second) => (signal) => Deferred.from.Promise(
5095
- Promise.all([Deferred.to.Promise(first(signal)), Deferred.to.Promise(second(signal))]).then(
5096
- ([va, vb]) => Validation.product(va, vb)
5097
- )
5098
- ),
5183
+ fold: (onFailed, onPassed) => (data) => isPassed(data) ? onPassed(data.value) : onFailed(data.errors),
5099
5184
  /**
5100
- * Runs all Task.Validations concurrently and collects results.
5101
- * If all are Passed, returns Passed with all values as an array.
5102
- * If any fail, returns Failed with all accumulated errors.
5185
+ * Pattern matches on a Validation, returning the result of the matching case.
5103
5186
  *
5104
5187
  * @example
5105
5188
  * ```ts
5106
- * await Task.Validation.productAll([
5107
- * validateName(form.name),
5108
- * validateEmail(form.email),
5109
- * validateAge(form.age),
5110
- * ])(); // Passed([name, email, age]) or Failed([...all errors])
5189
+ * pipe(
5190
+ * validation,
5191
+ * Validation.match({
5192
+ * passed: value => `Got ${value}`,
5193
+ * failed: errors => `Failed: ${errors.join(", ")}`
5194
+ * })
5195
+ * );
5111
5196
  * ```
5112
5197
  */
5113
- productAll: (data) => (signal) => Deferred.from.Promise(
5114
- Promise.all(data.map((t) => Deferred.to.Promise(t(signal)))).then((results) => {
5115
- const [first, ...rest] = results;
5116
- return Validation.productAll([first, ...rest]);
5117
- })
5118
- ),
5198
+ match: (cases) => (data) => isPassed(data) ? cases.passed(data.value) : cases.failed(data.errors),
5119
5199
  /**
5120
- * Transforms all accumulated errors inside a Task.Validation.
5200
+ * Returns the success value or a default value if the Validation is failed.
5201
+ * The default can be a different type, widening the result to `A | B`.
5121
5202
  *
5122
5203
  * @example
5123
5204
  * ```ts
5124
- * pipe(
5125
- * Task.Validation.failed("oops"),
5126
- * Task.Validation.mapError(e => e.toUpperCase())
5127
- * ); // Task.Validation(Failed(["OOPS"]))
5205
+ * pipe(Validation.make.passed(5), Validation.getOrElse(() => 0)); // 5
5206
+ * pipe(Validation.make.failed("oops"), Validation.getOrElse(() => 0)); // 0
5207
+ * pipe(Validation.make.failed("oops"), Validation.getOrElse(() => null)); // null — typed as number | null
5128
5208
  * ```
5129
5209
  */
5130
- mapError: (f) => (data) => Task.map(Validation.mapError(f))(data),
5210
+ getOrElse: (defaultValue) => (data) => isPassed(data) ? data.value : defaultValue(),
5131
5211
  /**
5132
- * Executes a side effect on the accumulated errors without changing the Task.Validation.
5212
+ * Executes a side effect on the success value without changing the Validation.
5133
5213
  *
5134
5214
  * @example
5135
5215
  * ```ts
5136
5216
  * pipe(
5137
- * Task.Validation.failed("invalid name"),
5138
- * Task.Validation.tapError(errs => logger.error(errs))
5217
+ * Validation.make.passed(5),
5218
+ * Validation.tap(n => console.log("Value:", n)),
5219
+ * Validation.map(n => n * 2)
5139
5220
  * );
5140
5221
  * ```
5141
5222
  */
5142
- tapError: (f) => (data) => Task.map(Validation.tapError(f))(data),
5223
+ tap: (f) => (data) => {
5224
+ if (isPassed(data)) {
5225
+ f(data.value);
5226
+ }
5227
+ return data;
5228
+ },
5143
5229
  /**
5144
- * Combines a record of Task.Validations into a single Task.Validation of a record.
5145
- * Evaluates fields in parallel and accumulates all validation errors.
5230
+ * Executes a side effect on the accumulated errors without changing the Validation.
5231
+ * Useful for logging or reporting validation failures.
5146
5232
  *
5147
5233
  * @example
5148
5234
  * ```ts
5149
- * Task.Validation.struct({
5150
- * name: Task.Validation.passed("Alice"),
5151
- * age: Task.Validation.passed(30)
5152
- * }); // Task.Validation({ name: "Alice", age: 30 })
5235
+ * pipe(
5236
+ * Validation.make.failed("Name required"),
5237
+ * Validation.tapError(errors => console.error("validation failed:", errors)),
5238
+ * Validation.map(toUser)
5239
+ * );
5153
5240
  * ```
5154
5241
  */
5155
- struct: (fields) => (signal) => Deferred.from.Promise((() => {
5156
- const keys = Object.keys(fields);
5157
- const promises = keys.map((key) => Deferred.to.Promise(fields[key](signal)));
5158
- return Promise.all(promises).then((results) => {
5159
- const record = {};
5160
- const errors = [];
5161
- for (let i = 0; i < keys.length; i++) {
5162
- const res = results[i];
5163
- if (Validation.is.passed(res)) {
5164
- record[keys[i]] = res.value;
5165
- } else {
5166
- errors.push(...res.errors);
5167
- }
5168
- }
5169
- return isNonEmptyArr(errors) ? Validation.make.failedAll(errors) : Validation.make.passed(record);
5170
- });
5171
- })()),
5242
+ tapError: (f) => (data) => {
5243
+ if (isFailed(data)) {
5244
+ f(data.errors);
5245
+ }
5246
+ return data;
5247
+ },
5172
5248
  /**
5173
- * Creates a memoized version of a Task.Validation. The task is executed at most once on first call,
5174
- * and its resolved Validation is cached for all subsequent calls.
5175
- *
5176
- * @example
5177
- * ```ts
5178
- * const validate = Task.Validation.memoize(validateFormTask);
5179
- * ```
5249
+ * Recovers from a Failed state by providing a fallback Validation.
5250
+ * The fallback receives the accumulated error list so callers can inspect which errors occurred.
5251
+ * The fallback can produce a different success type, widening the result to `Validation<E, A | B>`.
5180
5252
  */
5181
- memoize: (task) => Task.memoize(task)
5182
- };
5183
-
5184
- // src/Core/Task.ts
5185
- var toPromise2 = (task, signal) => Deferred.to.Promise(task(signal));
5186
- var fromPromise2 = (f) => (signal) => Deferred.from.Promise(f(signal));
5187
- var getMs2 = (duration) => Duration.to.milliseconds(duration);
5188
- var resolveTask = (value) => () => Deferred.from.Promise(globalThis.Promise.resolve(value));
5189
- var syncTask = (f) => () => Deferred.from.Promise(globalThis.Promise.resolve(f()));
5190
- var Task = {
5253
+ recover: (fallback) => (data) => isPassed(data) ? data : fallback(data.errors),
5191
5254
  /**
5192
- * Creates a Task that immediately resolves to the given value.
5255
+ * Recovers from a Failed state unless `isBlocked` returns true for any of the accumulated errors.
5256
+ * The fallback can produce a different success type, widening the result to `Validation<E, A | B>`.
5193
5257
  *
5194
5258
  * @example
5195
5259
  * ```ts
5196
- * const task = Task.resolve(42);
5197
- * const value = await task(); // 42
5260
+ * pipe(
5261
+ * Validation.make.failed("field-error"),
5262
+ * Validation.recoverUnless(e => e === "fatal", () => Validation.make.passed(0))
5263
+ * ); // Passed(0)
5198
5264
  * ```
5199
5265
  */
5200
- resolve: resolveTask,
5201
- // --- from ---
5202
- from: {
5266
+ recoverUnless: (isBlocked, fallback) => (data) => isFailed(data) && !data.errors.some(isBlocked) ? fallback() : data,
5267
+ // --- to ---
5268
+ to: {
5203
5269
  /**
5204
- * Creates a Task from a lazy synchronous thunk.
5205
- * Unlike `Task.resolve(f())`, `from.sync` does not evaluate `f` until the Task is called.
5270
+ * Converts a Validation to a Result.
5271
+ * Passed becomes Ok.
5272
+ * Direct call converts Failed to Err with accumulated error list `NonEmptyArr<E>`.
5273
+ * Curried call converts Failed to Err with combined error `E2` via `combineErrors`.
5206
5274
  *
5207
5275
  * @example
5208
5276
  * ```ts
5209
- * const t = Task.from.sync(() => Date.now()); // Date.now() not called yet
5210
- * const ts = await t(); // called here, every time
5277
+ * Validation.to.Result(Validation.make.passed(42)); // Ok(42)
5278
+ * Validation.to.Result(Validation.make.failed("oops")); // Err(["oops"])
5279
+ * pipe(Validation.make.failed("oops"), Validation.to.Result(errors => errors.join(", "))); // Err("oops")
5211
5280
  * ```
5212
5281
  */
5213
- sync: syncTask
5282
+ Result: toResult,
5283
+ /**
5284
+ * Converts a Validation to a Maybe. `Passed` becomes `Some`; `Failed` becomes `None`
5285
+ * (errors are discarded).
5286
+ *
5287
+ * @example
5288
+ * ```ts
5289
+ * Validation.to.Maybe(Validation.make.passed(42)); // Some(42)
5290
+ * Validation.to.Maybe(Validation.make.failed("bad")); // None
5291
+ * ```
5292
+ */
5293
+ Maybe: (data) => isPassed(data) ? Maybe.make.some(data.value) : Maybe.make.none()
5214
5294
  },
5215
5295
  /**
5216
- * Wraps a Promise-returning thunk that may throw or reject,
5217
- * trapping errors with a fallback function and returning a guaranteed `Task<A>`.
5296
+ * Combines two independent Validation instances into a tuple.
5297
+ * If both are Passed, returns Passed with both values as a tuple.
5298
+ * If either is Failed, accumulates errors from both sides.
5218
5299
  *
5219
5300
  * @example
5220
5301
  * ```ts
5221
- * const loadConfig = Task.tryCatch(
5222
- * () => configStore.get("default"),
5223
- * { onError: () => DEFAULT_CONFIG }
5224
- * );
5302
+ * Validation.product(
5303
+ * Validation.make.passed("alice"),
5304
+ * Validation.make.passed(30)
5305
+ * ); // Passed(["alice", 30])
5306
+ *
5307
+ * Validation.product(
5308
+ * Validation.make.failed("Name required"),
5309
+ * Validation.make.failed("Age must be >= 0")
5310
+ * ); // Failed(["Name required", "Age must be >= 0"])
5225
5311
  * ```
5226
5312
  */
5227
- tryCatch: (f, options) => fromPromise2((signal) => globalThis.Promise.resolve().then(() => f(signal)).catch((err2) => options.onError(err2))),
5313
+ product: (first, second) => {
5314
+ if (isPassed(first)) {
5315
+ return isPassed(second) ? makePassed([first.value, second.value]) : makeFailedAll(second.errors);
5316
+ }
5317
+ return isPassed(second) ? makeFailedAll(first.errors) : makeFailedAll([...first.errors, ...second.errors]);
5318
+ },
5228
5319
  /**
5229
- * Transforms the value inside a Task.
5320
+ * Combines a non-empty list of Validation instances, accumulating all errors.
5321
+ * If all are Passed, returns Passed with all values collected into an array.
5322
+ * If any are Failed, returns Failed with all accumulated errors.
5230
5323
  *
5231
5324
  * @example
5232
5325
  * ```ts
5233
- * pipe(
5234
- * Task.resolve(5),
5235
- * Task.map(n => n * 2)
5236
- * )(); // Deferred<10>
5326
+ * Validation.productAll([
5327
+ * validateName(name),
5328
+ * validateEmail(email),
5329
+ * validateAge(age)
5330
+ * ]);
5331
+ * // Passed([name, email, age]) or Failed([...all errors])
5237
5332
  * ```
5238
5333
  */
5239
- map: (f) => (data) => fromPromise2((signal) => toPromise2(data, signal).then(f)),
5334
+ productAll: (data) => {
5335
+ const values = [];
5336
+ const errors = [];
5337
+ for (const v of data) {
5338
+ if (isPassed(v)) {
5339
+ values.push(v.value);
5340
+ } else {
5341
+ errors.push(...v.errors);
5342
+ }
5343
+ }
5344
+ return isNonEmptyArr(errors) ? makeFailedAll(errors) : makePassed(values);
5345
+ },
5240
5346
  /**
5241
- * Chains Task computations. Passes the resolved value of the first Task to f.
5347
+ * Combines a record of Validations into a single Validation of a record.
5348
+ * Accumulates all failed branches' errors.
5242
5349
  *
5243
5350
  * @example
5244
5351
  * ```ts
5245
- * const readUserId: Task<string> = Task.resolve(session.userId);
5246
- * const loadPrefs = (id: string): Task<Preferences> =>
5247
- * Task.resolve(prefsCache.get(id));
5248
- *
5249
- * pipe(
5250
- * readUserId,
5251
- * Task.chain(loadPrefs)
5252
- * )(); // Deferred<Preferences>
5253
- * ```
5254
- */
5255
- chain: (f) => (data) => fromPromise2((signal) => toPromise2(data, signal).then((a) => toPromise2(f(a), signal))),
5256
- /**
5257
- * Applies a function wrapped in a Task to a value wrapped in a Task.
5258
- * Both Tasks run in parallel.
5352
+ * Validation.struct({
5353
+ * name: Validation.make.passed("Alice"),
5354
+ * age: Validation.make.passed(30)
5355
+ * }); // Passed({ name: "Alice", age: 30 })
5259
5356
  *
5260
- * @example
5261
- * ```ts
5262
- * const add = (a: number) => (b: number) => a + b;
5263
- * pipe(
5264
- * Task.resolve(add),
5265
- * Task.ap(Task.resolve(5)),
5266
- * Task.ap(Task.resolve(3))
5267
- * )(); // Deferred<8>
5357
+ * Validation.struct({
5358
+ * name: Validation.make.failed("Name required"),
5359
+ * age: Validation.make.failed("Age must be >= 0")
5360
+ * }); // Failed(["Name required", "Age must be >= 0"])
5268
5361
  * ```
5269
5362
  */
5270
- ap: (arg) => (data) => fromPromise2((signal) => Promise.all([toPromise2(data, signal), toPromise2(arg, signal)]).then(([f, a]) => f(a))),
5363
+ struct: (fields) => {
5364
+ const record = {};
5365
+ const errors = [];
5366
+ for (const key in fields) {
5367
+ if (Object.hasOwn(fields, key)) {
5368
+ const val = fields[key];
5369
+ if (isPassed(val)) {
5370
+ record[key] = val.value;
5371
+ } else {
5372
+ errors.push(...val.errors);
5373
+ }
5374
+ }
5375
+ }
5376
+ return isNonEmptyArr(errors) ? makeFailedAll(errors) : makePassed(record);
5377
+ }
5378
+ };
5379
+
5380
+ // src/Core/TaskValidation.ts
5381
+ var makePassed2 = (value) => Task.resolve(Validation.make.passed(value));
5382
+ var makeFailed2 = (error) => Task.resolve(Validation.make.failed(error));
5383
+ var makeFailedAll2 = (errors) => Task.resolve(Validation.make.failedAll(errors));
5384
+ var TaskValidation = {
5385
+ make: {
5386
+ /**
5387
+ * Wraps a value in a passed Task.Validation.
5388
+ *
5389
+ * @example
5390
+ * ```ts
5391
+ * const task = Task.Validation.make.passed(42);
5392
+ * const res = await task(); // Passed(42)
5393
+ * ```
5394
+ */
5395
+ passed: makePassed2,
5396
+ /**
5397
+ * Creates a failed Task.Validation with a single error.
5398
+ *
5399
+ * @example
5400
+ * ```ts
5401
+ * const task = Task.Validation.make.failed("invalid");
5402
+ * const res = await task(); // Failed(["invalid"])
5403
+ * ```
5404
+ */
5405
+ failed: makeFailed2,
5406
+ /**
5407
+ * Creates a failed Task.Validation from multiple errors.
5408
+ *
5409
+ * @example
5410
+ * ```ts
5411
+ * const task = Task.Validation.make.failedAll(["err1", "err2"]);
5412
+ * const res = await task(); // Failed(["err1", "err2"])
5413
+ * ```
5414
+ */
5415
+ failedAll: makeFailedAll2
5416
+ },
5417
+ // --- from ---
5418
+ from: {
5419
+ /**
5420
+ * Lifts a Validation into a Task.Validation.
5421
+ *
5422
+ * @example
5423
+ * ```ts
5424
+ * Task.Validation.from.Validation(Validation.make.passed(42));
5425
+ * ```
5426
+ */
5427
+ Validation: (validation) => Task.resolve(validation),
5428
+ /**
5429
+ * Creates a Task.Validation from a nullable value.
5430
+ * If the value is null or undefined, returns Failed with the error from onNull.
5431
+ * Otherwise, returns Passed.
5432
+ *
5433
+ * @example
5434
+ * ```ts
5435
+ * Task.Validation.from.nullable(() => "missing")(42); // resolves to Passed(42)
5436
+ * Task.Validation.from.nullable(() => "missing")(null); // resolves to Failed(["missing"])
5437
+ * ```
5438
+ */
5439
+ nullable: (onNull) => (value) => Task.resolve(
5440
+ value === null || value === void 0 ? Validation.make.failed(onNull()) : Validation.make.passed(value)
5441
+ ),
5442
+ /**
5443
+ * Creates a Task.Validation from a Maybe.
5444
+ * Some becomes Passed, None becomes Failed with the error from onNone.
5445
+ *
5446
+ * @example
5447
+ * ```ts
5448
+ * Task.Validation.from.Maybe(() => "empty")(Maybe.make.some(42)); // resolves to Passed(42)
5449
+ * Task.Validation.from.Maybe(() => "empty")(Maybe.make.none()); // resolves to Failed(["empty"])
5450
+ * ```
5451
+ */
5452
+ Maybe: (onNone) => (maybe) => Task.resolve(
5453
+ Maybe.is.none(maybe) ? Validation.make.failed(onNone()) : Validation.make.passed(maybe.value)
5454
+ ),
5455
+ /**
5456
+ * Creates a Task.Validation from a Result.
5457
+ * Ok becomes Passed, Err(e) becomes Failed([e]).
5458
+ *
5459
+ * @example
5460
+ * ```ts
5461
+ * Task.Validation.from.Result(Result.make.ok(42)); // resolves to Passed(42)
5462
+ * Task.Validation.from.Result(Result.make.err("bad")); // resolves to Failed(["bad"])
5463
+ * ```
5464
+ */
5465
+ Result: (result) => Task.resolve(Validation.from.Result(result))
5466
+ },
5467
+ // --- to ---
5468
+ to: {
5469
+ /**
5470
+ * Converts a `Task.Validation` to a `Task.Result`, combining accumulated errors using `combineErrors`.
5471
+ * `Passed(a)` becomes `Ok(a)`; `Failed(errors)` becomes `Err(combineErrors(errors))`.
5472
+ *
5473
+ * @example
5474
+ * ```ts
5475
+ * Task.Validation.to.Result((errors) => errors.join(", "))(validationTask);
5476
+ * ```
5477
+ */
5478
+ Result: (combineErrors) => (data) => Task.map(Validation.to.Result(combineErrors))(data),
5479
+ /**
5480
+ * Converts a `Task.Validation` to a `Task.Maybe`.
5481
+ * `Passed(a)` becomes `Some(a)`; `Failed(errors)` becomes `None` (errors are discarded).
5482
+ *
5483
+ * @example
5484
+ * ```ts
5485
+ * Task.Validation.to.Maybe(validationTask);
5486
+ * ```
5487
+ */
5488
+ Maybe: (data) => Task.map(Validation.to.Maybe)(data)
5489
+ },
5271
5490
  /**
5272
- * Executes a side effect on the value without changing the Task.
5273
- * Useful for logging or debugging.
5491
+ * Creates a Task.Validation from a Promise-returning thunk that may throw or reject.
5492
+ * Catches any errors and transforms them using the `onError` function into a Failed validation.
5493
+ * The thunk optionally receives an `AbortSignal` forwarded from the call site.
5274
5494
  *
5275
5495
  * @example
5276
5496
  * ```ts
5277
- * pipe(
5278
- * loadConfig,
5279
- * Task.tap(cfg => console.log("Config:", cfg)),
5280
- * Task.map(buildReport)
5497
+ * const loadConfig = Task.Validation.tryCatch(
5498
+ * (signal) => configStore.get("default", { signal }),
5499
+ * { onError: (e) => `Failed to load config: ${e}` }
5281
5500
  * );
5282
5501
  * ```
5283
5502
  */
5284
- tap: (f) => (data) => fromPromise2(
5285
- (signal) => toPromise2(data, signal).then((a) => {
5286
- f(a);
5287
- return a;
5288
- })
5503
+ tryCatch: (f, options) => (signal) => Deferred.from.Promise(
5504
+ // oxlint-disable-next-line require-await
5505
+ globalThis.Promise.resolve().then(async () => f(signal)).then(Validation.make.passed).catch(
5506
+ (error) => Validation.make.failed(options.onError(error))
5507
+ )
5289
5508
  ),
5290
5509
  /**
5291
- * Runs multiple Tasks in parallel and collects their results.
5292
- *
5293
- * @example
5294
- * ```ts
5295
- * Task.all([loadConfig, detectLocale, loadTheme])();
5296
- * // Deferred<[Config, string, Theme]>
5297
- * ```
5510
+ * Transforms the success value inside a Task.Validation.
5298
5511
  */
5299
- all: (tasks) => fromPromise2(
5300
- (signal) => Promise.all(tasks.map((t) => toPromise2(t, signal)))
5301
- ),
5512
+ map: (f) => (data) => Task.map(Validation.map(f))(data),
5302
5513
  /**
5303
- * Delays the execution of a Task by the specified duration.
5304
- * Useful for debouncing or rate limiting.
5514
+ * Applies a function wrapped in a Task.Validation to a value wrapped in a
5515
+ * Task.Validation. Both Tasks run in parallel and errors from both sides
5516
+ * are accumulated.
5305
5517
  *
5306
5518
  * @example
5307
5519
  * ```ts
5308
5520
  * pipe(
5309
- * Task.resolve(42),
5310
- * Task.delay(Duration.seconds(1))
5311
- * )(); // Resolves after 1 second
5521
+ * Task.Validation.make.passed((name: string) => (age: number) => ({ name, age })),
5522
+ * Task.Validation.ap(validateName(name)),
5523
+ * Task.Validation.ap(validateAge(age))
5524
+ * )();
5312
5525
  * ```
5313
5526
  */
5314
- delay: (duration) => (data) => fromPromise2(
5315
- (signal) => new Promise((res) => {
5316
- let timerId;
5317
- const onAbort = () => {
5318
- clearTimeout(timerId);
5319
- res(toPromise2(data, signal));
5320
- };
5321
- if (signal) {
5322
- if (signal.aborted) {
5323
- return res(toPromise2(data, signal));
5324
- }
5325
- signal.addEventListener("abort", onAbort, { once: true });
5326
- }
5327
- timerId = setTimeout(() => {
5328
- signal?.removeEventListener("abort", onAbort);
5329
- res(toPromise2(data, signal));
5330
- }, getMs2(duration));
5331
- })
5527
+ ap: (arg) => (data) => (signal) => Deferred.from.Promise(
5528
+ Promise.all([Deferred.to.Promise(data(signal)), Deferred.to.Promise(arg(signal))]).then(
5529
+ ([vf, va]) => Validation.ap(va)(vf)
5530
+ )
5332
5531
  ),
5333
5532
  /**
5334
- * Runs a Task a fixed number of times sequentially, collecting all results into an array.
5335
- * An optional delay duration can be inserted between runs.
5336
- *
5337
- * @example
5338
- * ```ts
5339
- * pipe(
5340
- * pollSensor,
5341
- * Task.repeat({ times: 5, delay: Duration.seconds(1) })
5342
- * )(); // Task<Reading[]> — 5 readings, one per second
5343
- * ```
5533
+ * Extracts a value from a Task.Validation by providing handlers for both cases.
5344
5534
  */
5345
- repeat: (options) => (task) => fromPromise2((signal) => {
5346
- const { times, delay: delayDuration } = options;
5347
- if (times <= 0) {
5348
- return Promise.resolve([]);
5349
- }
5350
- const results = [];
5351
- const wait = () => new Promise((r) => {
5352
- let timerId;
5353
- const onAbort = () => {
5354
- clearTimeout(timerId);
5355
- r();
5356
- };
5357
- if (signal) {
5358
- signal.addEventListener("abort", onAbort, { once: true });
5359
- }
5360
- timerId = setTimeout(() => {
5361
- signal?.removeEventListener("abort", onAbort);
5362
- r();
5363
- }, delayDuration ? getMs2(delayDuration) : 0);
5364
- });
5365
- const run = (left) => {
5366
- if (signal?.aborted) {
5367
- return Promise.resolve(results);
5368
- }
5369
- return toPromise2(task, signal).then((a) => {
5370
- results.push(a);
5371
- if (left <= 1 || signal?.aborted) {
5372
- return results;
5373
- }
5374
- return wait().then(() => run(left - 1));
5375
- });
5376
- };
5377
- return run(times);
5378
- }),
5535
+ fold: (onFailed, onPassed) => (data) => Task.map(Validation.fold(onFailed, onPassed))(data),
5379
5536
  /**
5380
- * Runs a Task repeatedly until the result satisfies a predicate, returning that result.
5381
- * An optional delay duration can be inserted between runs.
5382
- * An optional `maxAttempts` cap stops the loop after N calls — the last value is returned
5383
- * regardless of whether the predicate was satisfied.
5537
+ * Pattern matches on a Task.Validation, returning a Task of the result.
5384
5538
  *
5385
5539
  * @example
5386
5540
  * ```ts
5387
5541
  * pipe(
5388
- * checkStatus,
5389
- * Task.repeatUntil({ when: (s) => s === "ready", delay: Duration.milliseconds(500) })
5390
- * )(); // polls every 500ms until status is "ready"
5542
+ * validateForm(input),
5543
+ * Task.Validation.match({
5544
+ * passed: data => save(data),
5545
+ * failed: errors => showErrors(errors)
5546
+ * })
5547
+ * )();
5391
5548
  * ```
5392
5549
  */
5393
- repeatUntil: (options) => (task) => fromPromise2((signal) => {
5394
- const { when: predicate, delay: delayDuration, maxAttempts } = options;
5395
- const wait = () => new Promise((r) => {
5396
- let timerId;
5397
- const onAbort = () => {
5398
- clearTimeout(timerId);
5399
- r();
5400
- };
5401
- if (signal) {
5402
- signal.addEventListener("abort", onAbort, { once: true });
5403
- }
5404
- timerId = setTimeout(() => {
5405
- signal?.removeEventListener("abort", onAbort);
5406
- r();
5407
- }, delayDuration ? getMs2(delayDuration) : 0);
5408
- });
5409
- const run = (attempt, lastValue) => {
5410
- if (signal?.aborted && lastValue !== void 0) {
5411
- return Promise.resolve(lastValue);
5412
- }
5413
- return toPromise2(task, signal).then((a) => {
5414
- if (predicate(a)) {
5415
- return a;
5416
- }
5417
- if (maxAttempts !== void 0 && attempt >= maxAttempts) {
5418
- return a;
5419
- }
5420
- if (signal?.aborted) {
5421
- return a;
5422
- }
5423
- return wait().then(() => run(attempt + 1, a));
5424
- });
5425
- };
5426
- return run(1);
5427
- }),
5550
+ match: (cases) => (data) => Task.map(Validation.match(cases))(data),
5428
5551
  /**
5429
- * Resolves with the value of the first Task to complete. All Tasks start
5430
- * immediately. When one resolves, the other tasks are cancelled (aborted)
5431
- * downstream.
5432
- *
5433
- * @example
5434
- * ```ts
5435
- * const fast = Task.resolve("fast");
5436
- * const slow = Task.delay(Duration.milliseconds(200))(Task.resolve("slow"));
5437
- *
5438
- * await Task.race([fast, slow])(); // "fast"
5439
- * ```
5552
+ * Returns the success value or a default value if the Task.Validation is failed.
5553
+ * The default can be a different type, widening the result to `Task<A | B>`.
5440
5554
  */
5441
- race: (tasks) => {
5442
- if (tasks.length === 0) {
5443
- return () => Deferred.from.Promise(new Promise(() => {
5444
- }));
5445
- }
5446
- return fromPromise2((outerSignal) => {
5447
- const controllers = tasks.map(() => new AbortController());
5448
- const onOuterAbort = () => {
5449
- for (const ctrl of controllers) {
5450
- ctrl.abort();
5451
- }
5452
- };
5453
- if (outerSignal) {
5454
- if (outerSignal.aborted) {
5455
- onOuterAbort();
5456
- } else {
5457
- outerSignal.addEventListener("abort", onOuterAbort, { once: true });
5458
- }
5459
- }
5460
- const promises = tasks.map((task, idx) => {
5461
- const ctrl = controllers[idx];
5462
- return toPromise2(task, ctrl.signal).then((result) => {
5463
- for (let i = 0; i < controllers.length; i++) {
5464
- if (i !== idx) {
5465
- controllers[i].abort();
5466
- }
5467
- }
5468
- outerSignal?.removeEventListener("abort", onOuterAbort);
5469
- return result;
5470
- });
5471
- });
5472
- return Promise.race(promises);
5473
- });
5474
- },
5555
+ getOrElse: (defaultValue) => (data) => Task.map(Validation.getOrElse(defaultValue))(data),
5556
+ /**
5557
+ * Executes a side effect on the success value without changing the Task.Validation.
5558
+ * Useful for logging or debugging.
5559
+ */
5560
+ tap: (f) => (data) => Task.map(Validation.tap(f))(data),
5561
+ /**
5562
+ * Recovers from a Failed state by providing a fallback Task.Validation.
5563
+ * The fallback receives the accumulated error list so callers can inspect which errors occurred.
5564
+ * The fallback can produce a different success type, widening the result to `Task.Validation<E, A | B>`.
5565
+ */
5566
+ recover: (fallback) => (data) => Task.chain(
5567
+ (validation) => Validation.is.passed(validation) ? Task.resolve(validation) : fallback(validation.errors)
5568
+ )(data),
5475
5569
  /**
5476
- * Runs an array of Tasks concurrently and collects their results in an array.
5477
- * Forward-propagates the call site's AbortSignal to all subtasks concurrently.
5570
+ * Recovers from a Failed state unless the predicate `isBlocked` returns true for the accumulated errors.
5571
+ * The fallback receives the accumulated errors and can produce a different success type, widening the result to `Task.Validation<E, A | B>`.
5478
5572
  *
5479
5573
  * @example
5480
5574
  * ```ts
5481
- * Task.sequence([loadConfig, detectLocale, loadTheme])();
5482
- * // Deferred<[Config, string, Theme]>
5575
+ * pipe(
5576
+ * validationTask,
5577
+ * Task.Validation.recoverUnless(
5578
+ * (errors) => errors.includes("fatal"),
5579
+ * (errors) => Task.Validation.make.passed("fallback")
5580
+ * )
5581
+ * );
5483
5582
  * ```
5484
5583
  */
5485
- sequence: (tasks) => fromPromise2((signal) => Promise.all(tasks.map((t) => toPromise2(t, signal)))),
5584
+ recoverUnless: (isBlocked, fallback) => (data) => Task.chain(
5585
+ (validation) => Validation.is.passed(validation) ? Task.resolve(validation) : isBlocked(validation.errors) ? Task.resolve(validation) : fallback(validation.errors)
5586
+ )(data),
5486
5587
  /**
5487
- * Runs an array of Tasks one at a time in order, collecting all results.
5488
- * Each Task starts only after the previous one resolves.
5588
+ * Runs two Task.Validations concurrently and combines their results into a tuple.
5589
+ * If both are Passed, returns Passed with both values. If either fails, accumulates
5590
+ * errors from both sides.
5489
5591
  *
5490
5592
  * @example
5491
5593
  * ```ts
5492
- * let log: number[] = [];
5493
- * const makeTask = (n: number) => Task.resolve(n);
5594
+ * await Task.Validation.product(
5595
+ * validateName(form.name),
5596
+ * validateAge(form.age),
5597
+ * )(); // Passed(["Alice", 30]) or Failed([...errors])
5598
+ * ```
5599
+ */
5600
+ product: (first, second) => (signal) => Deferred.from.Promise(
5601
+ Promise.all([Deferred.to.Promise(first(signal)), Deferred.to.Promise(second(signal))]).then(
5602
+ ([va, vb]) => Validation.product(va, vb)
5603
+ )
5604
+ ),
5605
+ /**
5606
+ * Runs all Task.Validations concurrently and collects results.
5607
+ * If all are Passed, returns Passed with all values as an array.
5608
+ * If any fail, returns Failed with all accumulated errors.
5494
5609
  *
5495
- * await Task.sequential([makeTask(1), makeTask(2), makeTask(3)])();
5496
- * // log = [1, 2, 3] — tasks ran in order
5610
+ * @example
5611
+ * ```ts
5612
+ * await Task.Validation.productAll([
5613
+ * validateName(form.name),
5614
+ * validateEmail(form.email),
5615
+ * validateAge(form.age),
5616
+ * ])(); // Passed([name, email, age]) or Failed([...all errors])
5497
5617
  * ```
5498
5618
  */
5499
- sequential: (tasks) => fromPromise2(async (signal) => {
5500
- const results = [];
5501
- for (const task of tasks) {
5502
- if (signal?.aborted) {
5503
- break;
5504
- }
5505
- results.push(await toPromise2(task, signal));
5506
- }
5507
- return results;
5508
- }),
5619
+ productAll: (data) => (signal) => Deferred.from.Promise(
5620
+ Promise.all(data.map((t) => Deferred.to.Promise(t(signal)))).then((results) => {
5621
+ const [first, ...rest] = results;
5622
+ return Validation.productAll([first, ...rest]);
5623
+ })
5624
+ ),
5509
5625
  /**
5510
- * Converts a `Task<A>` into a `Task<Result<E, A>>`, resolving to `Err` if the
5511
- * Task does not complete within the given duration. The inner Task receives an
5512
- * `AbortSignal` that fires when the deadline passes, so asynchronous operations
5513
- * that accept a signal are cancelled rather than left dangling.
5626
+ * Transforms all accumulated errors inside a Task.Validation.
5514
5627
  *
5515
5628
  * @example
5516
5629
  * ```ts
5517
5630
  * pipe(
5518
- * heavyComputation,
5519
- * Task.timeout({ duration: Duration.seconds(5), onTimeout: () => "timed out" }),
5520
- * Task.Result.chain(processResult)
5521
- * );
5631
+ * Task.Validation.make.failed("oops"),
5632
+ * Task.Validation.mapError(e => e.toUpperCase())
5633
+ * ); // Task.Validation(Failed(["OOPS"]))
5522
5634
  * ```
5523
5635
  */
5524
- timeout: (options) => (task) => fromPromise2((outerSignal) => {
5525
- const { duration, onTimeout } = options;
5526
- const controller = new AbortController();
5527
- let timerId;
5528
- let cleanUp = () => {
5529
- };
5530
- const onOuterAbort = () => {
5531
- cleanUp();
5532
- controller.abort();
5533
- };
5534
- cleanUp = () => {
5535
- clearTimeout(timerId);
5536
- outerSignal?.removeEventListener("abort", onOuterAbort);
5537
- };
5538
- if (outerSignal) {
5539
- if (outerSignal.aborted) {
5540
- controller.abort();
5541
- } else {
5542
- outerSignal.addEventListener("abort", onOuterAbort, { once: true });
5543
- }
5544
- }
5545
- return Promise.race([
5546
- toPromise2(task, controller.signal).then((a) => {
5547
- cleanUp();
5548
- return Result.make.ok(a);
5549
- }),
5550
- new Promise((res) => {
5551
- timerId = setTimeout(() => {
5552
- controller.abort();
5553
- cleanUp();
5554
- res(Result.make.err(onTimeout()));
5555
- }, getMs2(duration));
5556
- })
5557
- ]);
5558
- }),
5636
+ mapError: (f) => (data) => Task.map(Validation.mapError(f))(data),
5559
5637
  /**
5560
- * Creates a Task paired with an `abort` handle. Calling `abort()` cancels the
5561
- * current in-flight call immediately. Unlike a one-shot abort, calling `task()`
5562
- * again after `abort()` starts a fresh call with a new signal.
5563
- *
5564
- * Each invocation of `task()` automatically cancels the previous in-flight call,
5565
- * making it safe to call repeatedly (e.g. on user input) without leaking promises.
5566
- *
5567
- * If an outer signal is also present (passed at the call site), aborting it
5568
- * propagates into the internal controller.
5638
+ * Executes a side effect on the accumulated errors without changing the Task.Validation.
5569
5639
  *
5570
5640
  * @example
5571
5641
  * ```ts
5572
- * const { task: poll, abort } = Task.abortable(
5573
- * (signal) => waitForEvent(bus, "ready", { signal }),
5642
+ * pipe(
5643
+ * Task.Validation.make.failed("invalid name"),
5644
+ * Task.Validation.tapError(errs => logger.error(errs))
5574
5645
  * );
5646
+ * ```
5647
+ */
5648
+ tapError: (f) => (data) => Task.map(Validation.tapError(f))(data),
5649
+ /**
5650
+ * Combines a record of Task.Validations into a single Task.Validation of a record.
5651
+ * Evaluates fields in parallel and accumulates all validation errors.
5575
5652
  *
5576
- * onUnmount(abort);
5577
- * await poll();
5653
+ * @example
5654
+ * ```ts
5655
+ * Task.Validation.struct({
5656
+ * name: Task.Validation.make.passed("Alice"),
5657
+ * age: Task.Validation.make.passed(30)
5658
+ * }); // Task.Validation({ name: "Alice", age: 30 })
5578
5659
  * ```
5579
5660
  */
5580
- abortable: (factory) => {
5581
- let currentController = null;
5582
- const abort = () => currentController?.abort();
5583
- const task = (outerSignal) => {
5584
- currentController?.abort();
5585
- currentController = new AbortController();
5586
- const controller = currentController;
5587
- if (outerSignal) {
5588
- if (outerSignal.aborted) {
5589
- controller.abort(outerSignal.reason);
5661
+ struct: (fields) => (signal) => Deferred.from.Promise((() => {
5662
+ const keys = Object.keys(fields);
5663
+ const promises = keys.map((key) => Deferred.to.Promise(fields[key](signal)));
5664
+ return Promise.all(promises).then((results) => {
5665
+ const record = {};
5666
+ const errors = [];
5667
+ for (let i = 0; i < keys.length; i++) {
5668
+ const res = results[i];
5669
+ if (Validation.is.passed(res)) {
5670
+ record[keys[i]] = res.value;
5590
5671
  } else {
5591
- outerSignal.addEventListener("abort", () => controller.abort(outerSignal.reason), { once: true });
5672
+ errors.push(...res.errors);
5592
5673
  }
5593
5674
  }
5594
- return Deferred.from.Promise(factory(controller.signal));
5595
- };
5596
- return { task, abort };
5675
+ return isNonEmptyArr(errors) ? Validation.make.failedAll(errors) : Validation.make.passed(record);
5676
+ });
5677
+ })()),
5678
+ /**
5679
+ * Creates a memoized version of a Task.Validation. The task is executed at most once on first call,
5680
+ * and its resolved Validation is cached for all subsequent calls.
5681
+ *
5682
+ * @example
5683
+ * ```ts
5684
+ * const validate = Task.Validation.memoize(validateFormTask);
5685
+ * ```
5686
+ */
5687
+ memoize: (task) => Task.memoize(task)
5688
+ };
5689
+
5690
+ // src/Core/Task.ts
5691
+ var toPromise2 = (task, signal) => Deferred.to.Promise(task(signal));
5692
+ var fromPromise2 = (f) => (signal) => Deferred.from.Promise(f(signal));
5693
+ var getMs2 = (duration) => Duration.to.milliseconds(duration);
5694
+ var resolveTask = (value) => () => Deferred.from.Promise(globalThis.Promise.resolve(value));
5695
+ var syncTask = (f) => () => Deferred.from.Promise(globalThis.Promise.resolve(f()));
5696
+ var Task = {
5697
+ /**
5698
+ * Creates a Task that immediately resolves to the given value.
5699
+ *
5700
+ * @example
5701
+ * ```ts
5702
+ * const task = Task.resolve(42);
5703
+ * const value = await task(); // 42
5704
+ * ```
5705
+ */
5706
+ resolve: resolveTask,
5707
+ // --- from ---
5708
+ from: {
5709
+ /**
5710
+ * Creates a Task from a lazy synchronous thunk.
5711
+ * Unlike `Task.resolve(f())`, `from.sync` does not evaluate `f` until the Task is called.
5712
+ *
5713
+ * @example
5714
+ * ```ts
5715
+ * const t = Task.from.sync(() => Date.now()); // Date.now() not called yet
5716
+ * const ts = await t(); // called here, every time
5717
+ * ```
5718
+ */
5719
+ sync: syncTask
5597
5720
  },
5598
5721
  /**
5599
- * Executes a task with an optional signal. Use as a terminal step in a `pipe` chain.
5722
+ * Wraps a Promise-returning thunk that may throw or reject,
5723
+ * trapping errors with a fallback function and returning a guaranteed `Task<A>`.
5600
5724
  *
5601
5725
  * @example
5602
5726
  * ```ts
5603
- * const name = await pipe(
5604
- * loadConfig,
5605
- * Task.map(config => config.name),
5606
- * Task.run(),
5727
+ * const loadConfig = Task.tryCatch(
5728
+ * () => configStore.get("default"),
5729
+ * { onError: () => DEFAULT_CONFIG }
5607
5730
  * );
5608
5731
  * ```
5609
5732
  */
5610
- run: (signal) => (task) => task(signal),
5733
+ tryCatch: (f, options) => fromPromise2((signal) => globalThis.Promise.resolve().then(() => f(signal)).catch((err2) => options.onError(err2))),
5611
5734
  /**
5612
- * Converts a Task value into an object containing a single property.
5613
- * Initiates the pipeline accumulator record.
5735
+ * Transforms the value inside a Task.
5614
5736
  *
5615
5737
  * @example
5616
5738
  * ```ts
5617
- * pipe(Task.resolve(42), Task.bindTo("value")); // Task({ value: 42 })
5739
+ * pipe(
5740
+ * Task.resolve(5),
5741
+ * Task.map(n => n * 2)
5742
+ * )(); // Deferred<10>
5618
5743
  * ```
5619
5744
  */
5620
- bindTo: (key) => (data) => fromPromise2((signal) => toPromise2(data, signal).then((a) => ({ [key]: a }))),
5745
+ map: (f) => (data) => fromPromise2((signal) => toPromise2(data, signal).then(f)),
5621
5746
  /**
5622
- * Evaluates a new Task using the current accumulator and attaches the output to a new key.
5747
+ * Chains Task computations. Passes the resolved value of the first Task to f.
5623
5748
  *
5624
5749
  * @example
5625
5750
  * ```ts
5751
+ * const readUserId: Task<string> = Task.resolve(session.userId);
5752
+ * const loadPrefs = (id: string): Task<Preferences> =>
5753
+ * Task.resolve(prefsCache.get(id));
5754
+ *
5626
5755
  * pipe(
5627
- * Task.resolve({ a: 1 }),
5628
- * Task.bind("b", ({ a }) => Task.resolve(a + 1))
5629
- * ); // Task({ a: 1, b: 2 })
5756
+ * readUserId,
5757
+ * Task.chain(loadPrefs)
5758
+ * )(); // Deferred<Preferences>
5759
+ * ```
5760
+ */
5761
+ chain: (f) => (data) => fromPromise2((signal) => toPromise2(data, signal).then((a) => toPromise2(f(a), signal))),
5762
+ /**
5763
+ * Applies a function wrapped in a Task to a value wrapped in a Task.
5764
+ * Both Tasks run in parallel.
5765
+ *
5766
+ * @example
5767
+ * ```ts
5768
+ * const add = (a: number) => (b: number) => a + b;
5769
+ * pipe(
5770
+ * Task.resolve(add),
5771
+ * Task.ap(Task.resolve(5)),
5772
+ * Task.ap(Task.resolve(3))
5773
+ * )(); // Deferred<8>
5630
5774
  * ```
5631
5775
  */
5632
- bind: (key, f) => (data) => fromPromise2(
5633
- (signal) => toPromise2(data, signal).then(
5634
- (a) => toPromise2(f(a), signal).then((b) => ({ ...a, [key]: b }))
5635
- )
5636
- ),
5776
+ ap: (arg) => (data) => fromPromise2((signal) => Promise.all([toPromise2(data, signal), toPromise2(arg, signal)]).then(([f, a]) => f(a))),
5637
5777
  /**
5638
- * Creates a memoized version of a Task. The task is executed at most once on first call,
5639
- * and its resolved value is cached for all subsequent calls.
5778
+ * Executes a side effect on the value without changing the Task.
5779
+ * Useful for logging or debugging.
5640
5780
  *
5641
5781
  * @example
5642
5782
  * ```ts
5643
- * const loadToken = Task.memoize(loadAuthToken);
5644
- * const token1 = await loadToken(); // loads token
5645
- * const token2 = await loadToken(); // returns cached token immediately
5783
+ * pipe(
5784
+ * loadConfig,
5785
+ * Task.tap(cfg => console.log("Config:", cfg)),
5786
+ * Task.map(buildReport)
5787
+ * );
5646
5788
  * ```
5647
5789
  */
5648
- memoize: (task) => {
5649
- let cached = null;
5650
- return (signal) => {
5651
- if (cached === null) {
5652
- cached = task(signal);
5653
- }
5654
- return cached;
5655
- };
5656
- },
5790
+ tap: (f) => (data) => fromPromise2(
5791
+ (signal) => toPromise2(data, signal).then((a) => {
5792
+ f(a);
5793
+ return a;
5794
+ })
5795
+ ),
5657
5796
  /**
5658
- * Monitors progress of a Task by calling `onProgress(0)` before execution and `onProgress(1)` upon completion.
5797
+ * Runs multiple Tasks in parallel and collects their results.
5659
5798
  *
5660
5799
  * @example
5661
5800
  * ```ts
5662
- * const taskWithProgress = pipe(
5663
- * readTask,
5664
- * Task.withProgress((ratio) => console.log(`Progress: ${ratio * 100}%`))
5665
- * );
5801
+ * Task.all([loadConfig, detectLocale, loadTheme])();
5802
+ * // Deferred<[Config, string, Theme]>
5666
5803
  * ```
5667
5804
  */
5668
- withProgress: (onProgress) => (task) => (signal) => {
5669
- onProgress(0);
5670
- const d = task(signal);
5671
- return Deferred.from.Promise(
5672
- Deferred.to.Promise(d).then((res) => {
5673
- onProgress(1);
5674
- return res;
5675
- })
5676
- );
5677
- },
5805
+ all: (tasks) => fromPromise2(
5806
+ (signal) => Promise.all(tasks.map((t) => toPromise2(t, signal)))
5807
+ ),
5678
5808
  /**
5679
- * Attaches a read-only `.label` property to a Task, preserving the literal string generic type for IDE tooltips.
5809
+ * Delays the execution of a Task by the specified duration.
5810
+ * Useful for debouncing or rate limiting.
5680
5811
  *
5681
5812
  * @example
5682
5813
  * ```ts
5683
- * const labeledTask = pipe(readTask, Task.withLabel("readUser"));
5684
- * console.log(labeledTask.label); // "readUser"
5814
+ * pipe(
5815
+ * Task.resolve(42),
5816
+ * Task.delay(Duration.seconds(1))
5817
+ * )(); // Resolves after 1 second
5685
5818
  * ```
5686
5819
  */
5687
- withLabel: (label) => (task) => {
5688
- const fn = ((signal) => task(signal));
5689
- Object.defineProperty(fn, "label", { value: label, writable: false, enumerable: true, configurable: true });
5690
- return fn;
5691
- },
5692
- Maybe: TaskMaybe,
5693
- Result: TaskResult,
5694
- Validation: TaskValidation
5695
- };
5696
-
5697
- // src/Core/These.ts
5698
- var makeFirst = (value) => ({ kind: "First", first: value });
5699
- var makeSecond = (value) => ({ kind: "Second", second: value });
5700
- var makeBoth = (f, s) => ({ kind: "Both", first: f, second: s });
5701
- var isFirst = (data) => data.kind === "First";
5702
- var isSecond = (data) => data.kind === "Second";
5703
- var isBoth = (data) => data.kind === "Both";
5704
- var hasFirst = (data) => data.kind === "First" || data.kind === "Both";
5705
- var hasSecond = (data) => data.kind === "Second" || data.kind === "Both";
5706
- var These = {
5707
- make: {
5708
- /**
5709
- * Creates a These holding only a first value.
5710
- *
5711
- * @example
5712
- * ```ts
5713
- * These.make.first(42); // { kind: "First", first: 42 }
5714
- * ```
5715
- */
5716
- first: makeFirst,
5717
- /**
5718
- * Creates a These holding only a second value.
5719
- *
5720
- * @example
5721
- * ```ts
5722
- * These.make.second("warning"); // { kind: "Second", second: "warning" }
5723
- * ```
5724
- */
5725
- second: makeSecond,
5726
- /**
5727
- * Creates a These holding both a first and a second value simultaneously.
5728
- *
5729
- * @example
5730
- * ```ts
5731
- * These.make.both(42, "Deprecated API used"); // { kind: "Both", first: 42, second: "Deprecated API used" }
5732
- * ```
5733
- */
5734
- both: makeBoth
5735
- },
5736
- is: {
5737
- /**
5738
- * Type guard — checks if a These holds only a first value.
5739
- *
5740
- * @example
5741
- * ```ts
5742
- * const val = These.make.first(42);
5743
- * if (These.is.first(val)) {
5744
- * console.log(val.first); // 42
5745
- * }
5746
- * ```
5747
- */
5748
- first: isFirst,
5749
- /**
5750
- * Type guard — checks if a These holds only a second value.
5751
- *
5752
- * @example
5753
- * ```ts
5754
- * const val = These.make.second("warning");
5755
- * if (These.is.second(val)) {
5756
- * console.log(val.second); // "warning"
5757
- * }
5758
- * ```
5759
- */
5760
- second: isSecond,
5761
- /**
5762
- * Type guard — checks if a These holds both values simultaneously.
5763
- *
5764
- * @example
5765
- * ```ts
5766
- * const val = These.make.both(42, "warning");
5767
- * if (These.is.both(val)) {
5768
- * console.log(val.first, val.second); // 42 "warning"
5769
- * }
5770
- * ```
5771
- */
5772
- both: isBoth
5773
- },
5820
+ delay: (duration) => (data) => fromPromise2(
5821
+ (signal) => new Promise((res) => {
5822
+ let timerId;
5823
+ const onAbort = () => {
5824
+ clearTimeout(timerId);
5825
+ res(toPromise2(data, signal));
5826
+ };
5827
+ if (signal) {
5828
+ if (signal.aborted) {
5829
+ return res(toPromise2(data, signal));
5830
+ }
5831
+ signal.addEventListener("abort", onAbort, { once: true });
5832
+ }
5833
+ timerId = setTimeout(() => {
5834
+ signal?.removeEventListener("abort", onAbort);
5835
+ res(toPromise2(data, signal));
5836
+ }, getMs2(duration));
5837
+ })
5838
+ ),
5774
5839
  /**
5775
- * Returns true if the These contains a first value (First or Both).
5840
+ * Runs a Task a fixed number of times sequentially, collecting all results into an array.
5841
+ * An optional delay duration can be inserted between runs.
5776
5842
  *
5777
5843
  * @example
5778
5844
  * ```ts
5779
- * These.hasFirst(These.make.first(42)); // true
5780
- * These.hasFirst(These.make.both(42, "warn"));// true
5781
- * These.hasFirst(These.make.second("warn")); // false
5845
+ * pipe(
5846
+ * pollSensor,
5847
+ * Task.repeat({ times: 5, delay: Duration.seconds(1) })
5848
+ * )(); // Task<Reading[]> — 5 readings, one per second
5782
5849
  * ```
5783
5850
  */
5784
- hasFirst,
5851
+ repeat: (options) => (task) => fromPromise2((signal) => {
5852
+ const { times, delay: delayDuration } = options;
5853
+ if (times <= 0) {
5854
+ return Promise.resolve([]);
5855
+ }
5856
+ const results = [];
5857
+ const wait = () => new Promise((r) => {
5858
+ let timerId;
5859
+ const onAbort = () => {
5860
+ clearTimeout(timerId);
5861
+ r();
5862
+ };
5863
+ if (signal) {
5864
+ signal.addEventListener("abort", onAbort, { once: true });
5865
+ }
5866
+ timerId = setTimeout(() => {
5867
+ signal?.removeEventListener("abort", onAbort);
5868
+ r();
5869
+ }, delayDuration ? getMs2(delayDuration) : 0);
5870
+ });
5871
+ const run = (left) => {
5872
+ if (signal?.aborted) {
5873
+ return Promise.resolve(results);
5874
+ }
5875
+ return toPromise2(task, signal).then((a) => {
5876
+ results.push(a);
5877
+ if (left <= 1 || signal?.aborted) {
5878
+ return results;
5879
+ }
5880
+ return wait().then(() => run(left - 1));
5881
+ });
5882
+ };
5883
+ return run(times);
5884
+ }),
5785
5885
  /**
5786
- * Returns true if the These contains a second value (Second or Both).
5886
+ * Runs a Task repeatedly until the result satisfies a predicate, returning that result.
5887
+ * An optional delay duration can be inserted between runs.
5888
+ * An optional `maxAttempts` cap stops the loop after N calls — the last value is returned
5889
+ * regardless of whether the predicate was satisfied.
5787
5890
  *
5788
5891
  * @example
5789
5892
  * ```ts
5790
- * These.hasSecond(These.make.second("warn")); // true
5791
- * These.hasSecond(These.make.both(42, "warn"));// true
5792
- * These.hasSecond(These.make.first(42)); // false
5893
+ * pipe(
5894
+ * checkStatus,
5895
+ * Task.repeatUntil({ when: (s) => s === "ready", delay: Duration.milliseconds(500) })
5896
+ * )(); // polls every 500ms until status is "ready"
5793
5897
  * ```
5794
5898
  */
5795
- hasSecond,
5899
+ repeatUntil: (options) => (task) => fromPromise2((signal) => {
5900
+ const { when: predicate, delay: delayDuration, maxAttempts } = options;
5901
+ const wait = () => new Promise((r) => {
5902
+ let timerId;
5903
+ const onAbort = () => {
5904
+ clearTimeout(timerId);
5905
+ r();
5906
+ };
5907
+ if (signal) {
5908
+ signal.addEventListener("abort", onAbort, { once: true });
5909
+ }
5910
+ timerId = setTimeout(() => {
5911
+ signal?.removeEventListener("abort", onAbort);
5912
+ r();
5913
+ }, delayDuration ? getMs2(delayDuration) : 0);
5914
+ });
5915
+ const run = (attempt, lastValue) => {
5916
+ if (signal?.aborted && lastValue !== void 0) {
5917
+ return Promise.resolve(lastValue);
5918
+ }
5919
+ return toPromise2(task, signal).then((a) => {
5920
+ if (predicate(a)) {
5921
+ return a;
5922
+ }
5923
+ if (maxAttempts !== void 0 && attempt >= maxAttempts) {
5924
+ return a;
5925
+ }
5926
+ if (signal?.aborted) {
5927
+ return a;
5928
+ }
5929
+ return wait().then(() => run(attempt + 1, a));
5930
+ });
5931
+ };
5932
+ return run(1);
5933
+ }),
5796
5934
  /**
5797
- * Transforms the first value, leaving the second unchanged.
5935
+ * Resolves with the value of the first Task to complete. All Tasks start
5936
+ * immediately. When one resolves, the other tasks are cancelled (aborted)
5937
+ * downstream.
5798
5938
  *
5799
5939
  * @example
5800
5940
  * ```ts
5801
- * pipe(These.make.first(5), These.mapFirst(n => n * 2)); // First(10)
5802
- * pipe(These.make.both(5, "warn"), These.mapFirst(n => n * 2)); // Both(10, "warn")
5803
- * pipe(These.make.second("warn"), These.mapFirst(n => n * 2)); // Second("warn")
5941
+ * const fast = Task.resolve("fast");
5942
+ * const slow = Task.delay(Duration.milliseconds(200))(Task.resolve("slow"));
5943
+ *
5944
+ * await Task.race([fast, slow])(); // "fast"
5804
5945
  * ```
5805
5946
  */
5806
- mapFirst: (f) => (data) => {
5807
- if (isSecond(data)) {
5808
- return data;
5809
- }
5810
- if (isFirst(data)) {
5811
- return makeFirst(f(data.first));
5947
+ race: (tasks) => {
5948
+ if (tasks.length === 0) {
5949
+ return () => Deferred.from.Promise(new Promise(() => {
5950
+ }));
5812
5951
  }
5813
- return makeBoth(f(data.first), data.second);
5952
+ return fromPromise2((outerSignal) => {
5953
+ const controllers = tasks.map(() => new AbortController());
5954
+ const onOuterAbort = () => {
5955
+ for (const ctrl of controllers) {
5956
+ ctrl.abort();
5957
+ }
5958
+ };
5959
+ if (outerSignal) {
5960
+ if (outerSignal.aborted) {
5961
+ onOuterAbort();
5962
+ } else {
5963
+ outerSignal.addEventListener("abort", onOuterAbort, { once: true });
5964
+ }
5965
+ }
5966
+ const promises = tasks.map((task, idx) => {
5967
+ const ctrl = controllers[idx];
5968
+ return toPromise2(task, ctrl.signal).then((result) => {
5969
+ for (let i = 0; i < controllers.length; i++) {
5970
+ if (i !== idx) {
5971
+ controllers[i].abort();
5972
+ }
5973
+ }
5974
+ outerSignal?.removeEventListener("abort", onOuterAbort);
5975
+ return result;
5976
+ });
5977
+ });
5978
+ return Promise.race(promises);
5979
+ });
5814
5980
  },
5815
5981
  /**
5816
- * Transforms the second value, leaving the first unchanged.
5982
+ * Runs an array of Tasks concurrently and collects their results in an array.
5983
+ * Forward-propagates the call site's AbortSignal to all subtasks concurrently.
5817
5984
  *
5818
5985
  * @example
5819
5986
  * ```ts
5820
- * pipe(These.make.second("warn"), These.mapSecond(e => e.toUpperCase())); // Second("WARN")
5821
- * pipe(These.make.both(5, "warn"), These.mapSecond(e => e.toUpperCase())); // Both(5, "WARN")
5987
+ * Task.sequence([loadConfig, detectLocale, loadTheme])();
5988
+ * // Deferred<[Config, string, Theme]>
5822
5989
  * ```
5823
5990
  */
5824
- mapSecond: (f) => (data) => {
5825
- if (isFirst(data)) {
5826
- return data;
5827
- }
5828
- if (isSecond(data)) {
5829
- return makeSecond(f(data.second));
5830
- }
5831
- return makeBoth(data.first, f(data.second));
5832
- },
5991
+ sequence: (tasks) => fromPromise2((signal) => Promise.all(tasks.map((t) => toPromise2(t, signal)))),
5833
5992
  /**
5834
- * Transforms both the first and second values independently.
5993
+ * Runs an array of Tasks one at a time in order, collecting all results.
5994
+ * Each Task starts only after the previous one resolves.
5835
5995
  *
5836
5996
  * @example
5837
5997
  * ```ts
5838
- * pipe(
5839
- * These.make.both(5, "warn"),
5840
- * These.mapBoth(n => n * 2, e => e.toUpperCase())
5841
- * ); // Both(10, "WARN")
5998
+ * let log: number[] = [];
5999
+ * const makeTask = (n: number) => Task.resolve(n);
6000
+ *
6001
+ * await Task.sequential([makeTask(1), makeTask(2), makeTask(3)])();
6002
+ * // log = [1, 2, 3] — tasks ran in order
5842
6003
  * ```
5843
6004
  */
5844
- mapBoth: (onFirst, onSecond) => (data) => {
5845
- if (isSecond(data)) {
5846
- return makeSecond(onSecond(data.second));
5847
- }
5848
- if (isFirst(data)) {
5849
- return makeFirst(onFirst(data.first));
6005
+ sequential: (tasks) => fromPromise2(async (signal) => {
6006
+ const results = [];
6007
+ for (const task of tasks) {
6008
+ if (signal?.aborted) {
6009
+ break;
6010
+ }
6011
+ results.push(await toPromise2(task, signal));
5850
6012
  }
5851
- return makeBoth(onFirst(data.first), onSecond(data.second));
5852
- },
6013
+ return results;
6014
+ }),
5853
6015
  /**
5854
- * Chains These computations by passing the first value to f.
5855
- * Second propagates unchanged; First and Both apply f to the first value.
6016
+ * Converts a `Task<A>` into a `Task<Result<E, A>>`, resolving to `Err` if the
6017
+ * Task does not complete within the given duration. The inner Task receives an
6018
+ * `AbortSignal` that fires when the deadline passes, so asynchronous operations
6019
+ * that accept a signal are cancelled rather than left dangling.
5856
6020
  *
5857
6021
  * @example
5858
6022
  * ```ts
5859
- * const double = (n: number): These<number, string> => These.make.first(n * 2);
5860
- *
5861
- * pipe(These.make.first(5), These.chainFirst(double)); // First(10)
5862
- * pipe(These.make.both(5, "warn"), These.chainFirst(double)); // First(10)
5863
- * pipe(These.make.second("warn"), These.chainFirst(double)); // Second("warn")
6023
+ * pipe(
6024
+ * heavyComputation,
6025
+ * Task.timeout({ duration: Duration.seconds(5), onTimeout: () => "timed out" }),
6026
+ * Task.Result.chain(processResult)
6027
+ * );
5864
6028
  * ```
5865
6029
  */
5866
- chainFirst: (f) => (data) => {
5867
- if (isSecond(data)) {
5868
- return data;
6030
+ timeout: (options) => (task) => fromPromise2((outerSignal) => {
6031
+ const { duration, onTimeout } = options;
6032
+ const controller = new AbortController();
6033
+ let timerId;
6034
+ let cleanUp = () => {
6035
+ };
6036
+ const onOuterAbort = () => {
6037
+ cleanUp();
6038
+ controller.abort();
6039
+ };
6040
+ cleanUp = () => {
6041
+ clearTimeout(timerId);
6042
+ outerSignal?.removeEventListener("abort", onOuterAbort);
6043
+ };
6044
+ if (outerSignal) {
6045
+ if (outerSignal.aborted) {
6046
+ controller.abort();
6047
+ } else {
6048
+ outerSignal.addEventListener("abort", onOuterAbort, { once: true });
6049
+ }
5869
6050
  }
5870
- return f(data.first);
5871
- },
6051
+ return Promise.race([
6052
+ toPromise2(task, controller.signal).then((a) => {
6053
+ cleanUp();
6054
+ return Result.make.ok(a);
6055
+ }),
6056
+ new Promise((res) => {
6057
+ timerId = setTimeout(() => {
6058
+ controller.abort();
6059
+ cleanUp();
6060
+ res(Result.make.err(onTimeout()));
6061
+ }, getMs2(duration));
6062
+ })
6063
+ ]);
6064
+ }),
5872
6065
  /**
5873
- * Chains These computations by passing the second value to f.
5874
- * First propagates unchanged; Second and Both apply f to the second value.
6066
+ * Creates a Task paired with an `abort` handle. Calling `abort()` cancels the
6067
+ * current in-flight call immediately. Unlike a one-shot abort, calling `task()`
6068
+ * again after `abort()` starts a fresh call with a new signal.
6069
+ *
6070
+ * Each invocation of `task()` automatically cancels the previous in-flight call,
6071
+ * making it safe to call repeatedly (e.g. on user input) without leaking promises.
6072
+ *
6073
+ * If an outer signal is also present (passed at the call site), aborting it
6074
+ * propagates into the internal controller.
5875
6075
  *
5876
6076
  * @example
5877
6077
  * ```ts
5878
- * const shout = (s: string): These<number, string> => These.make.second(s.toUpperCase());
6078
+ * const { task: poll, abort } = Task.abortable(
6079
+ * (signal) => waitForEvent(bus, "ready", { signal }),
6080
+ * );
5879
6081
  *
5880
- * pipe(These.make.second("warn"), These.chainSecond(shout)); // Second("WARN")
5881
- * pipe(These.make.both(5, "warn"), These.chainSecond(shout)); // Second("WARN")
5882
- * pipe(These.make.first(5), These.chainSecond(shout)); // First(5)
6082
+ * onUnmount(abort);
6083
+ * await poll();
5883
6084
  * ```
5884
6085
  */
5885
- chainSecond: (f) => (data) => {
5886
- if (isFirst(data)) {
5887
- return data;
5888
- }
5889
- return f(data.second);
6086
+ abortable: (factory) => {
6087
+ let currentController = null;
6088
+ const abort = () => currentController?.abort();
6089
+ const task = (outerSignal) => {
6090
+ currentController?.abort();
6091
+ currentController = new AbortController();
6092
+ const controller = currentController;
6093
+ if (outerSignal) {
6094
+ if (outerSignal.aborted) {
6095
+ controller.abort(outerSignal.reason);
6096
+ } else {
6097
+ outerSignal.addEventListener("abort", () => controller.abort(outerSignal.reason), { once: true });
6098
+ }
6099
+ }
6100
+ return Deferred.from.Promise(factory(controller.signal));
6101
+ };
6102
+ return { task, abort };
5890
6103
  },
5891
6104
  /**
5892
- * Extracts a value from a These by providing handlers for all three cases.
6105
+ * Executes a task with an optional signal. Use as a terminal step in a `pipe` chain.
5893
6106
  *
5894
6107
  * @example
5895
6108
  * ```ts
5896
- * pipe(
5897
- * these,
5898
- * These.fold(
5899
- * a => `First: ${a}`,
5900
- * b => `Second: ${b}`,
5901
- * (a, b) => `Both: ${a} / ${b}`
5902
- * )
6109
+ * const name = await pipe(
6110
+ * loadConfig,
6111
+ * Task.map(config => config.name),
6112
+ * Task.run(),
5903
6113
  * );
5904
6114
  * ```
5905
6115
  */
5906
- fold: (onFirst, onSecond, onBoth) => (data) => {
5907
- if (isSecond(data)) {
5908
- return onSecond(data.second);
5909
- }
5910
- if (isFirst(data)) {
5911
- return onFirst(data.first);
5912
- }
5913
- return onBoth(data.first, data.second);
5914
- },
6116
+ run: (signal) => (task) => task(signal),
5915
6117
  /**
5916
- * Pattern matches on a These, returning the result of the matching case.
6118
+ * Converts a Task value into an object containing a single property.
6119
+ * Initiates the pipeline accumulator record.
5917
6120
  *
5918
6121
  * @example
5919
6122
  * ```ts
5920
- * pipe(
5921
- * these,
5922
- * These.match({
5923
- * first: a => `First: ${a}`,
5924
- * second: b => `Second: ${b}`,
5925
- * both: (a, b) => `Both: ${a} / ${b}`
5926
- * })
5927
- * );
6123
+ * pipe(Task.resolve(42), Task.bindTo("value")); // Task({ value: 42 })
5928
6124
  * ```
5929
6125
  */
5930
- match: (cases) => (data) => {
5931
- if (isSecond(data)) {
5932
- return cases.second(data.second);
5933
- }
5934
- if (isFirst(data)) {
5935
- return cases.first(data.first);
5936
- }
5937
- return cases.both(data.first, data.second);
5938
- },
6126
+ bindTo: (key) => (data) => fromPromise2((signal) => toPromise2(data, signal).then((a) => ({ [key]: a }))),
5939
6127
  /**
5940
- * Returns the first value, or a default if the These has no first value.
5941
- * The default can be a different type, widening the result to `A | C`.
5942
- *
5943
- * @example
5944
- * ```ts
5945
- * pipe(These.make.first(5), These.getFirstOrElse(() => 0)); // 5
5946
- * pipe(These.make.both(5, "warn"), These.getFirstOrElse(() => 0)); // 5
5947
- * pipe(These.make.second("warn"), These.getFirstOrElse(() => 0)); // 0
5948
- * pipe(These.make.second("warn"), These.getFirstOrElse(() => null)); // null — typed as number | null
6128
+ * Evaluates a new Task using the current accumulator and attaches the output to a new key.
6129
+ *
6130
+ * @example
6131
+ * ```ts
6132
+ * pipe(
6133
+ * Task.resolve({ a: 1 }),
6134
+ * Task.bind("b", ({ a }) => Task.resolve(a + 1))
6135
+ * ); // Task({ a: 1, b: 2 })
5949
6136
  * ```
5950
6137
  */
5951
- getFirstOrElse: (defaultValue) => (data) => hasFirst(data) ? data.first : defaultValue(),
6138
+ bind: (key, f) => (data) => fromPromise2(
6139
+ (signal) => toPromise2(data, signal).then(
6140
+ (a) => toPromise2(f(a), signal).then((b) => ({ ...a, [key]: b }))
6141
+ )
6142
+ ),
5952
6143
  /**
5953
- * Returns the second value, or a default if the These has no second value.
5954
- * The default can be a different type, widening the result to `B | D`.
6144
+ * Creates a memoized version of a Task. The task is executed at most once on first call,
6145
+ * and its resolved value is cached for all subsequent calls.
5955
6146
  *
5956
6147
  * @example
5957
6148
  * ```ts
5958
- * pipe(These.make.second("warn"), These.getSecondOrElse(() => "none")); // "warn"
5959
- * pipe(These.make.both(5, "warn"), These.getSecondOrElse(() => "none")); // "warn"
5960
- * pipe(These.make.first(5), These.getSecondOrElse(() => "none")); // "none"
5961
- * pipe(These.make.first(5), These.getSecondOrElse(() => null)); // null — typed as string | null
6149
+ * const loadToken = Task.memoize(loadAuthToken);
6150
+ * const token1 = await loadToken(); // loads token
6151
+ * const token2 = await loadToken(); // returns cached token immediately
5962
6152
  * ```
5963
6153
  */
5964
- getSecondOrElse: (defaultValue) => (data) => hasSecond(data) ? data.second : defaultValue(),
6154
+ memoize: (task) => {
6155
+ let cached = null;
6156
+ return (signal) => {
6157
+ if (cached === null) {
6158
+ cached = task(signal);
6159
+ }
6160
+ return cached;
6161
+ };
6162
+ },
5965
6163
  /**
5966
- * Runs a side effect on the first value without changing the These.
5967
- * Useful for logging or debugging.
6164
+ * Monitors progress of a Task by calling `onProgress(0)` before execution and `onProgress(1)` upon completion.
5968
6165
  *
5969
6166
  * @example
5970
6167
  * ```ts
5971
- * pipe(These.make.first(5), These.tap(console.log)); // logs 5, returns First(5)
6168
+ * const taskWithProgress = pipe(
6169
+ * readTask,
6170
+ * Task.withProgress((ratio) => console.log(`Progress: ${ratio * 100}%`))
6171
+ * );
5972
6172
  * ```
5973
6173
  */
5974
- tap: (f) => (data) => {
5975
- if (hasFirst(data)) {
5976
- f(data.first);
5977
- }
5978
- return data;
6174
+ withProgress: (onProgress) => (task) => (signal) => {
6175
+ onProgress(0);
6176
+ const d = task(signal);
6177
+ return Deferred.from.Promise(
6178
+ Deferred.to.Promise(d).then((res) => {
6179
+ onProgress(1);
6180
+ return res;
6181
+ })
6182
+ );
5979
6183
  },
5980
6184
  /**
5981
- * Swaps the roles of first and second values.
5982
- * - First(a) → Second(a)
5983
- * - Second(b) → First(b)
5984
- * - Both(a, b) → Both(b, a)
6185
+ * Attaches a read-only `.label` property to a Task, preserving the literal string generic type for IDE tooltips.
5985
6186
  *
5986
6187
  * @example
5987
6188
  * ```ts
5988
- * These.swap(These.make.first(5)); // Second(5)
5989
- * These.swap(These.make.second("warn")); // First("warn")
5990
- * These.swap(These.make.both(5, "warn")); // Both("warn", 5)
6189
+ * const labeledTask = pipe(readTask, Task.withLabel("readUser"));
6190
+ * console.log(labeledTask.label); // "readUser"
5991
6191
  * ```
5992
6192
  */
5993
- swap: (data) => {
5994
- if (isSecond(data)) {
5995
- return makeFirst(data.second);
5996
- }
5997
- if (isFirst(data)) {
5998
- return makeSecond(data.first);
5999
- }
6000
- return makeBoth(data.second, data.first);
6001
- }
6193
+ withLabel: (label) => (task) => {
6194
+ const fn = ((signal) => task(signal));
6195
+ Object.defineProperty(fn, "label", { value: label, writable: false, enumerable: true, configurable: true });
6196
+ return fn;
6197
+ },
6198
+ Maybe: TaskMaybe,
6199
+ Result: TaskResult,
6200
+ Validation: TaskValidation
6002
6201
  };
6003
6202
 
6004
- // src/Core/Validation.ts
6005
- var makePassed2 = (value) => ({ kind: "Passed", value });
6006
- var makeFailed2 = (error) => ({ kind: "Failed", errors: [error] });
6007
- var makeFailedAll2 = (errors) => ({ kind: "Failed", errors });
6008
- var isPassed = (data) => data.kind === "Passed";
6009
- var isFailed = (data) => data.kind === "Failed";
6010
- function toResult(arg) {
6011
- if (typeof arg === "function") {
6012
- const combine = arg;
6013
- return (val) => isPassed(val) ? Result.make.ok(val.value) : Result.make.err(combine(val.errors));
6014
- }
6015
- return isPassed(arg) ? Result.make.ok(arg.value) : Result.make.err(arg.errors);
6016
- }
6017
- var Validation = {
6203
+ // src/Core/These.ts
6204
+ var makeFirst = (value) => ({ kind: "First", first: value });
6205
+ var makeSecond = (value) => ({ kind: "Second", second: value });
6206
+ var makeBoth = (f, s) => ({ kind: "Both", first: f, second: s });
6207
+ var isFirst = (data) => data.kind === "First";
6208
+ var isSecond = (data) => data.kind === "Second";
6209
+ var isBoth = (data) => data.kind === "Both";
6210
+ var hasFirst = (data) => data.kind === "First" || data.kind === "Both";
6211
+ var hasSecond = (data) => data.kind === "Second" || data.kind === "Both";
6212
+ var These = {
6018
6213
  make: {
6019
6214
  /**
6020
- * Wraps a value in a passed Validation.
6215
+ * Creates a These holding only a first value.
6021
6216
  *
6022
6217
  * @example
6023
6218
  * ```ts
6024
- * Validation.make.passed(42); // Passed(42)
6219
+ * These.make.first(42); // { kind: "First", first: 42 }
6025
6220
  * ```
6026
6221
  */
6027
- passed: makePassed2,
6222
+ first: makeFirst,
6028
6223
  /**
6029
- * Creates a failed Validation from a single error.
6224
+ * Creates a These holding only a second value.
6030
6225
  *
6031
6226
  * @example
6032
6227
  * ```ts
6033
- * Validation.make.failed("Invalid input");
6228
+ * These.make.second("warning"); // { kind: "Second", second: "warning" }
6034
6229
  * ```
6035
6230
  */
6036
- failed: makeFailed2,
6231
+ second: makeSecond,
6037
6232
  /**
6038
- * Creates a failed Validation from multiple errors.
6233
+ * Creates a These holding both a first and a second value simultaneously.
6039
6234
  *
6040
6235
  * @example
6041
6236
  * ```ts
6042
- * Validation.make.failedAll(["Invalid input"]);
6237
+ * These.make.both(42, "Deprecated API used"); // { kind: "Both", first: 42, second: "Deprecated API used" }
6043
6238
  * ```
6044
6239
  */
6045
- failedAll: makeFailedAll2
6240
+ both: makeBoth
6046
6241
  },
6047
6242
  is: {
6048
6243
  /**
6049
- * Type guard that checks if a Validation is passed.
6244
+ * Type guard checks if a These holds only a first value.
6050
6245
  *
6051
6246
  * @example
6052
6247
  * ```ts
6053
- * const v = Validation.make.passed(42);
6054
- * if (Validation.is.passed(v)) {
6055
- * console.log(v.value); // 42
6248
+ * const val = These.make.first(42);
6249
+ * if (These.is.first(val)) {
6250
+ * console.log(val.first); // 42
6056
6251
  * }
6057
6252
  * ```
6058
6253
  */
6059
- passed: isPassed,
6254
+ first: isFirst,
6060
6255
  /**
6061
- * Type guard that checks if a Validation is failed.
6256
+ * Type guard checks if a These holds only a second value.
6062
6257
  *
6063
6258
  * @example
6064
6259
  * ```ts
6065
- * const v = Validation.make.failed("invalid");
6066
- * if (Validation.is.failed(v)) {
6067
- * console.log(v.errors); // ["invalid"]
6260
+ * const val = These.make.second("warning");
6261
+ * if (These.is.second(val)) {
6262
+ * console.log(val.second); // "warning"
6068
6263
  * }
6069
6264
  * ```
6070
6265
  */
6071
- failed: isFailed
6072
- },
6073
- /**
6074
- * Creates a Validation from a synchronous thunk that may throw.
6075
- * Catches any errors and transforms them using the `onError` function into a Failed validation.
6076
- *
6077
- * @example
6078
- * ```ts
6079
- * const result = Validation.tryCatch(
6080
- * () => JSON.parse(rawString),
6081
- * { onError: (e) => `Parse error: ${e}` }
6082
- * );
6083
- * ```
6084
- */
6085
- tryCatch: (f, options) => {
6086
- try {
6087
- return makePassed2(f());
6088
- } catch (error) {
6089
- return makeFailed2(options.onError(error));
6090
- }
6091
- },
6092
- // --- from ---
6093
- from: {
6094
- /**
6095
- * Creates a Validation from a predicate applied to a value.
6096
- * Returns Passed if the predicate passes, Failed from `onFalse` otherwise.
6097
- *
6098
- * @example
6099
- * ```ts
6100
- * const validateName = Validation.from.Predicate(
6101
- * (s: string) => s.length > 0,
6102
- * () => "Name is required"
6103
- * );
6104
- *
6105
- * validateName("Alice"); // Passed("Alice")
6106
- * validateName(""); // Failed(["Name is required"])
6107
- * ```
6108
- */
6109
- Predicate: (pred, onFalse) => (a) => pred(a) ? makePassed2(a) : makeFailed2(onFalse(a)),
6110
- /**
6111
- * Creates a Validation from a nullable value.
6112
- * If the value is null or undefined, returns Failed with the error from onNull.
6113
- * Otherwise, returns Passed.
6114
- *
6115
- * @example
6116
- * ```ts
6117
- * pipe(null, Validation.from.nullable(() => "is null")); // Failed(["is null"])
6118
- * pipe(42, Validation.from.nullable(() => "is null")); // Passed(42)
6119
- * ```
6120
- */
6121
- nullable: (onNull) => (value) => value === null || value === void 0 ? makeFailed2(onNull()) : makePassed2(value),
6122
- /**
6123
- * Creates a Validation from a Maybe.
6124
- * If the Maybe is None, returns Failed with the error from onNone.
6125
- * Otherwise, returns Passed.
6126
- *
6127
- * @example
6128
- * ```ts
6129
- * pipe(Maybe.make.none(), Validation.from.Maybe(() => "is none")); // Failed(["is none"])
6130
- * pipe(Maybe.make.some(42), Validation.from.Maybe(() => "is none")); // Passed(42)
6131
- * ```
6132
- */
6133
- Maybe: (onNone) => (maybe) => Maybe.is.none(maybe) ? makeFailed2(onNone()) : makePassed2(maybe.value),
6266
+ second: isSecond,
6134
6267
  /**
6135
- * Converts a `Result` to a `Validation`. `Ok` becomes `Passed`; `Err(e)` becomes `Failed([e])`.
6136
- *
6137
- * Useful when bridging from error-short-circuiting `Result` pipelines into
6138
- * error-accumulating `Validation` pipelines.
6268
+ * Type guard checks if a These holds both values simultaneously.
6139
6269
  *
6140
6270
  * @example
6141
6271
  * ```ts
6142
- * Validation.from.Result(Result.make.ok(42)); // Passed(42)
6143
- * Validation.from.Result(Result.make.err("bad")); // Failed(["bad"])
6272
+ * const val = These.make.both(42, "warning");
6273
+ * if (These.is.both(val)) {
6274
+ * console.log(val.first, val.second); // 42 "warning"
6275
+ * }
6144
6276
  * ```
6145
6277
  */
6146
- Result: (data) => data.kind === "Ok" ? makePassed2(data.value) : makeFailed2(data.error)
6278
+ both: isBoth
6147
6279
  },
6148
6280
  /**
6149
- * Transforms the success value inside a Validation.
6281
+ * Returns true if the These contains a first value (First or Both).
6150
6282
  *
6151
6283
  * @example
6152
6284
  * ```ts
6153
- * pipe(Validation.make.passed(5), Validation.map(n => n * 2)); // Passed(10)
6154
- * pipe(Validation.make.failed("oops"), Validation.map(n => n * 2)); // Failed(["oops"])
6285
+ * These.hasFirst(These.make.first(42)); // true
6286
+ * These.hasFirst(These.make.both(42, "warn"));// true
6287
+ * These.hasFirst(These.make.second("warn")); // false
6155
6288
  * ```
6156
6289
  */
6157
- map: (f) => (data) => isPassed(data) ? makePassed2(f(data.value)) : data,
6290
+ hasFirst,
6158
6291
  /**
6159
- * Transforms the error list inside a Validation.
6292
+ * Returns true if the These contains a second value (Second or Both).
6160
6293
  *
6161
6294
  * @example
6162
6295
  * ```ts
6163
- * pipe(Validation.make.failed("oops"), Validation.mapError(e => e.toUpperCase())); // Failed(["OOPS"])
6296
+ * These.hasSecond(These.make.second("warn")); // true
6297
+ * These.hasSecond(These.make.both(42, "warn"));// true
6298
+ * These.hasSecond(These.make.first(42)); // false
6164
6299
  * ```
6165
6300
  */
6166
- mapError: (f) => (data) => isFailed(data) ? makeFailedAll2(data.errors.map(f)) : data,
6301
+ hasSecond,
6167
6302
  /**
6168
- * Applies a function wrapped in a Validation to a value wrapped in a Validation.
6169
- * Accumulates errors from both sides.
6303
+ * Transforms the first value, leaving the second unchanged.
6170
6304
  *
6171
6305
  * @example
6172
6306
  * ```ts
6173
- * const add = (a: number) => (b: number) => a + b;
6174
- * pipe(
6175
- * Validation.make.passed(add),
6176
- * Validation.ap(Validation.make.passed(5)),
6177
- * Validation.ap(Validation.make.passed(3))
6178
- * ); // Passed(8)
6179
- *
6180
- * pipe(
6181
- * Validation.make.passed(add),
6182
- * Validation.ap(Validation.make.failed<string>("bad a")),
6183
- * Validation.ap(Validation.make.failed<string>("bad b"))
6184
- * ); // Failed(["bad a", "bad b"])
6307
+ * pipe(These.make.first(5), These.mapFirst(n => n * 2)); // First(10)
6308
+ * pipe(These.make.both(5, "warn"), These.mapFirst(n => n * 2)); // Both(10, "warn")
6309
+ * pipe(These.make.second("warn"), These.mapFirst(n => n * 2)); // Second("warn")
6185
6310
  * ```
6186
6311
  */
6187
- ap: (arg) => (data) => {
6188
- if (isPassed(data)) {
6189
- return isPassed(arg) ? makePassed2(data.value(arg.value)) : makeFailedAll2(arg.errors);
6312
+ mapFirst: (f) => (data) => {
6313
+ if (isSecond(data)) {
6314
+ return data;
6315
+ }
6316
+ if (isFirst(data)) {
6317
+ return makeFirst(f(data.first));
6190
6318
  }
6191
- return isPassed(arg) ? makeFailedAll2(data.errors) : makeFailedAll2([...data.errors, ...arg.errors]);
6319
+ return makeBoth(f(data.first), data.second);
6192
6320
  },
6193
- /**
6194
- * Applies a function wrapped in a Validation to a value wrapped in a Validation,
6195
- * using a custom error concatenator function when both sides fail.
6321
+ /**
6322
+ * Transforms the second value, leaving the first unchanged.
6196
6323
  *
6197
6324
  * @example
6198
6325
  * ```ts
6199
- * const concat = (e1: NonEmptyArr<string>, e2: NonEmptyArr<string>): NonEmptyArr<string> =>
6200
- * [...e1, ...e2];
6201
- * pipe(fnVal, Validation.apCustom(concat)(argVal));
6326
+ * pipe(These.make.second("warn"), These.mapSecond(e => e.toUpperCase())); // Second("WARN")
6327
+ * pipe(These.make.both(5, "warn"), These.mapSecond(e => e.toUpperCase())); // Both(5, "WARN")
6202
6328
  * ```
6203
6329
  */
6204
- apCustom: (concat) => (arg) => (data) => {
6205
- if (isPassed(data)) {
6206
- return isPassed(arg) ? makePassed2(data.value(arg.value)) : makeFailedAll2(arg.errors);
6330
+ mapSecond: (f) => (data) => {
6331
+ if (isFirst(data)) {
6332
+ return data;
6207
6333
  }
6208
- return isPassed(arg) ? makeFailedAll2(data.errors) : makeFailedAll2(concat(data.errors, arg.errors));
6334
+ if (isSecond(data)) {
6335
+ return makeSecond(f(data.second));
6336
+ }
6337
+ return makeBoth(data.first, f(data.second));
6209
6338
  },
6210
6339
  /**
6211
- * Extracts the value from a Validation by providing handlers for both cases.
6340
+ * Transforms both the first and second values independently.
6212
6341
  *
6213
6342
  * @example
6214
6343
  * ```ts
6215
6344
  * pipe(
6216
- * Validation.make.passed(42),
6217
- * Validation.fold(
6218
- * errors => `Errors: ${errors.join(", ")}`,
6219
- * value => `Value: ${value}`
6220
- * )
6221
- * );
6345
+ * These.make.both(5, "warn"),
6346
+ * These.mapBoth(n => n * 2, e => e.toUpperCase())
6347
+ * ); // Both(10, "WARN")
6222
6348
  * ```
6223
6349
  */
6224
- fold: (onFailed, onPassed) => (data) => isPassed(data) ? onPassed(data.value) : onFailed(data.errors),
6350
+ mapBoth: (onFirst, onSecond) => (data) => {
6351
+ if (isSecond(data)) {
6352
+ return makeSecond(onSecond(data.second));
6353
+ }
6354
+ if (isFirst(data)) {
6355
+ return makeFirst(onFirst(data.first));
6356
+ }
6357
+ return makeBoth(onFirst(data.first), onSecond(data.second));
6358
+ },
6225
6359
  /**
6226
- * Pattern matches on a Validation, returning the result of the matching case.
6360
+ * Chains These computations by passing the first value to f.
6361
+ * Second propagates unchanged; First and Both apply f to the first value.
6227
6362
  *
6228
6363
  * @example
6229
6364
  * ```ts
6230
- * pipe(
6231
- * validation,
6232
- * Validation.match({
6233
- * passed: value => `Got ${value}`,
6234
- * failed: errors => `Failed: ${errors.join(", ")}`
6235
- * })
6236
- * );
6365
+ * const double = (n: number): These<number, string> => These.make.first(n * 2);
6366
+ *
6367
+ * pipe(These.make.first(5), These.chainFirst(double)); // First(10)
6368
+ * pipe(These.make.both(5, "warn"), These.chainFirst(double)); // First(10)
6369
+ * pipe(These.make.second("warn"), These.chainFirst(double)); // Second("warn")
6237
6370
  * ```
6238
6371
  */
6239
- match: (cases) => (data) => isPassed(data) ? cases.passed(data.value) : cases.failed(data.errors),
6372
+ chainFirst: (f) => (data) => {
6373
+ if (isSecond(data)) {
6374
+ return data;
6375
+ }
6376
+ return f(data.first);
6377
+ },
6240
6378
  /**
6241
- * Returns the success value or a default value if the Validation is failed.
6242
- * The default can be a different type, widening the result to `A | B`.
6379
+ * Chains These computations by passing the second value to f.
6380
+ * First propagates unchanged; Second and Both apply f to the second value.
6243
6381
  *
6244
6382
  * @example
6245
6383
  * ```ts
6246
- * pipe(Validation.make.passed(5), Validation.getOrElse(() => 0)); // 5
6247
- * pipe(Validation.make.failed("oops"), Validation.getOrElse(() => 0)); // 0
6248
- * pipe(Validation.make.failed("oops"), Validation.getOrElse(() => null)); // null — typed as number | null
6384
+ * const shout = (s: string): These<number, string> => These.make.second(s.toUpperCase());
6385
+ *
6386
+ * pipe(These.make.second("warn"), These.chainSecond(shout)); // Second("WARN")
6387
+ * pipe(These.make.both(5, "warn"), These.chainSecond(shout)); // Second("WARN")
6388
+ * pipe(These.make.first(5), These.chainSecond(shout)); // First(5)
6249
6389
  * ```
6250
6390
  */
6251
- getOrElse: (defaultValue) => (data) => isPassed(data) ? data.value : defaultValue(),
6391
+ chainSecond: (f) => (data) => {
6392
+ if (isFirst(data)) {
6393
+ return data;
6394
+ }
6395
+ return f(data.second);
6396
+ },
6252
6397
  /**
6253
- * Executes a side effect on the success value without changing the Validation.
6398
+ * Extracts a value from a These by providing handlers for all three cases.
6254
6399
  *
6255
6400
  * @example
6256
6401
  * ```ts
6257
6402
  * pipe(
6258
- * Validation.make.passed(5),
6259
- * Validation.tap(n => console.log("Value:", n)),
6260
- * Validation.map(n => n * 2)
6403
+ * these,
6404
+ * These.fold(
6405
+ * a => `First: ${a}`,
6406
+ * b => `Second: ${b}`,
6407
+ * (a, b) => `Both: ${a} / ${b}`
6408
+ * )
6261
6409
  * );
6262
6410
  * ```
6263
6411
  */
6264
- tap: (f) => (data) => {
6265
- if (isPassed(data)) {
6266
- f(data.value);
6412
+ fold: (onFirst, onSecond, onBoth) => (data) => {
6413
+ if (isSecond(data)) {
6414
+ return onSecond(data.second);
6267
6415
  }
6268
- return data;
6416
+ if (isFirst(data)) {
6417
+ return onFirst(data.first);
6418
+ }
6419
+ return onBoth(data.first, data.second);
6269
6420
  },
6270
6421
  /**
6271
- * Executes a side effect on the accumulated errors without changing the Validation.
6272
- * Useful for logging or reporting validation failures.
6422
+ * Pattern matches on a These, returning the result of the matching case.
6273
6423
  *
6274
6424
  * @example
6275
6425
  * ```ts
6276
6426
  * pipe(
6277
- * Validation.make.failed("Name required"),
6278
- * Validation.tapError(errors => console.error("validation failed:", errors)),
6279
- * Validation.map(toUser)
6427
+ * these,
6428
+ * These.match({
6429
+ * first: a => `First: ${a}`,
6430
+ * second: b => `Second: ${b}`,
6431
+ * both: (a, b) => `Both: ${a} / ${b}`
6432
+ * })
6280
6433
  * );
6281
6434
  * ```
6282
6435
  */
6283
- tapError: (f) => (data) => {
6284
- if (isFailed(data)) {
6285
- f(data.errors);
6436
+ match: (cases) => (data) => {
6437
+ if (isSecond(data)) {
6438
+ return cases.second(data.second);
6286
6439
  }
6287
- return data;
6440
+ if (isFirst(data)) {
6441
+ return cases.first(data.first);
6442
+ }
6443
+ return cases.both(data.first, data.second);
6288
6444
  },
6289
6445
  /**
6290
- * Recovers from a Failed state by providing a fallback Validation.
6291
- * The fallback receives the accumulated error list so callers can inspect which errors occurred.
6292
- * The fallback can produce a different success type, widening the result to `Validation<E, A | B>`.
6293
- */
6294
- recover: (fallback) => (data) => isPassed(data) ? data : fallback(data.errors),
6295
- /**
6296
- * Recovers from a Failed state unless `isBlocked` returns true for any of the accumulated errors.
6297
- * The fallback can produce a different success type, widening the result to `Validation<E, A | B>`.
6446
+ * Returns the first value, or a default if the These has no first value.
6447
+ * The default can be a different type, widening the result to `A | C`.
6298
6448
  *
6299
6449
  * @example
6300
6450
  * ```ts
6301
- * pipe(
6302
- * Validation.make.failed("field-error"),
6303
- * Validation.recoverUnless(e => e === "fatal", () => Validation.make.passed(0))
6304
- * ); // Passed(0)
6451
+ * pipe(These.make.first(5), These.getFirstOrElse(() => 0)); // 5
6452
+ * pipe(These.make.both(5, "warn"), These.getFirstOrElse(() => 0)); // 5
6453
+ * pipe(These.make.second("warn"), These.getFirstOrElse(() => 0)); // 0
6454
+ * pipe(These.make.second("warn"), These.getFirstOrElse(() => null)); // null — typed as number | null
6305
6455
  * ```
6306
6456
  */
6307
- recoverUnless: (isBlocked, fallback) => (data) => isFailed(data) && !data.errors.some(isBlocked) ? fallback() : data,
6308
- // --- to ---
6309
- to: {
6310
- /**
6311
- * Converts a Validation to a Result.
6312
- * Passed becomes Ok.
6313
- * Direct call converts Failed to Err with accumulated error list `NonEmptyArr<E>`.
6314
- * Curried call converts Failed to Err with combined error `E2` via `combineErrors`.
6315
- *
6316
- * @example
6317
- * ```ts
6318
- * Validation.to.Result(Validation.make.passed(42)); // Ok(42)
6319
- * Validation.to.Result(Validation.make.failed("oops")); // Err(["oops"])
6320
- * pipe(Validation.make.failed("oops"), Validation.to.Result(errors => errors.join(", "))); // Err("oops")
6321
- * ```
6322
- */
6323
- Result: toResult,
6324
- /**
6325
- * Converts a Validation to a Maybe. `Passed` becomes `Some`; `Failed` becomes `None`
6326
- * (errors are discarded).
6327
- *
6328
- * @example
6329
- * ```ts
6330
- * Validation.to.Maybe(Validation.make.passed(42)); // Some(42)
6331
- * Validation.to.Maybe(Validation.make.failed("bad")); // None
6332
- * ```
6333
- */
6334
- Maybe: (data) => isPassed(data) ? Maybe.make.some(data.value) : Maybe.make.none()
6335
- },
6457
+ getFirstOrElse: (defaultValue) => (data) => hasFirst(data) ? data.first : defaultValue(),
6336
6458
  /**
6337
- * Combines two independent Validation instances into a tuple.
6338
- * If both are Passed, returns Passed with both values as a tuple.
6339
- * If either is Failed, accumulates errors from both sides.
6459
+ * Returns the second value, or a default if the These has no second value.
6460
+ * The default can be a different type, widening the result to `B | D`.
6340
6461
  *
6341
6462
  * @example
6342
6463
  * ```ts
6343
- * Validation.product(
6344
- * Validation.make.passed("alice"),
6345
- * Validation.make.passed(30)
6346
- * ); // Passed(["alice", 30])
6347
- *
6348
- * Validation.product(
6349
- * Validation.make.failed("Name required"),
6350
- * Validation.make.failed("Age must be >= 0")
6351
- * ); // Failed(["Name required", "Age must be >= 0"])
6464
+ * pipe(These.make.second("warn"), These.getSecondOrElse(() => "none")); // "warn"
6465
+ * pipe(These.make.both(5, "warn"), These.getSecondOrElse(() => "none")); // "warn"
6466
+ * pipe(These.make.first(5), These.getSecondOrElse(() => "none")); // "none"
6467
+ * pipe(These.make.first(5), These.getSecondOrElse(() => null)); // null — typed as string | null
6352
6468
  * ```
6353
6469
  */
6354
- product: (first, second) => {
6355
- if (isPassed(first)) {
6356
- return isPassed(second) ? makePassed2([first.value, second.value]) : makeFailedAll2(second.errors);
6357
- }
6358
- return isPassed(second) ? makeFailedAll2(first.errors) : makeFailedAll2([...first.errors, ...second.errors]);
6359
- },
6470
+ getSecondOrElse: (defaultValue) => (data) => hasSecond(data) ? data.second : defaultValue(),
6360
6471
  /**
6361
- * Combines a non-empty list of Validation instances, accumulating all errors.
6362
- * If all are Passed, returns Passed with all values collected into an array.
6363
- * If any are Failed, returns Failed with all accumulated errors.
6472
+ * Runs a side effect on the first value without changing the These.
6473
+ * Useful for logging or debugging.
6364
6474
  *
6365
6475
  * @example
6366
6476
  * ```ts
6367
- * Validation.productAll([
6368
- * validateName(name),
6369
- * validateEmail(email),
6370
- * validateAge(age)
6371
- * ]);
6372
- * // Passed([name, email, age]) or Failed([...all errors])
6477
+ * pipe(These.make.first(5), These.tap(console.log)); // logs 5, returns First(5)
6373
6478
  * ```
6374
6479
  */
6375
- productAll: (data) => {
6376
- const values = [];
6377
- const errors = [];
6378
- for (const v of data) {
6379
- if (isPassed(v)) {
6380
- values.push(v.value);
6381
- } else {
6382
- errors.push(...v.errors);
6383
- }
6480
+ tap: (f) => (data) => {
6481
+ if (hasFirst(data)) {
6482
+ f(data.first);
6384
6483
  }
6385
- return isNonEmptyArr(errors) ? makeFailedAll2(errors) : makePassed2(values);
6484
+ return data;
6386
6485
  },
6387
6486
  /**
6388
- * Combines a record of Validations into a single Validation of a record.
6389
- * Accumulates all failed branches' errors.
6487
+ * Swaps the roles of first and second values.
6488
+ * - First(a) → Second(a)
6489
+ * - Second(b) → First(b)
6490
+ * - Both(a, b) → Both(b, a)
6390
6491
  *
6391
6492
  * @example
6392
6493
  * ```ts
6393
- * Validation.struct({
6394
- * name: Validation.make.passed("Alice"),
6395
- * age: Validation.make.passed(30)
6396
- * }); // Passed({ name: "Alice", age: 30 })
6397
- *
6398
- * Validation.struct({
6399
- * name: Validation.make.failed("Name required"),
6400
- * age: Validation.make.failed("Age must be >= 0")
6401
- * }); // Failed(["Name required", "Age must be >= 0"])
6494
+ * These.swap(These.make.first(5)); // Second(5)
6495
+ * These.swap(These.make.second("warn")); // First("warn")
6496
+ * These.swap(These.make.both(5, "warn")); // Both("warn", 5)
6402
6497
  * ```
6403
6498
  */
6404
- struct: (fields) => {
6405
- const record = {};
6406
- const errors = [];
6407
- for (const key in fields) {
6408
- if (Object.hasOwn(fields, key)) {
6409
- const val = fields[key];
6410
- if (isPassed(val)) {
6411
- record[key] = val.value;
6412
- } else {
6413
- errors.push(...val.errors);
6414
- }
6415
- }
6499
+ swap: (data) => {
6500
+ if (isSecond(data)) {
6501
+ return makeFirst(data.second);
6416
6502
  }
6417
- return isNonEmptyArr(errors) ? makeFailedAll2(errors) : makePassed2(record);
6503
+ if (isFirst(data)) {
6504
+ return makeSecond(data.first);
6505
+ }
6506
+ return makeBoth(data.second, data.first);
6418
6507
  }
6419
6508
  };
6420
6509
  export {