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