onlyfuncs 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +138 -2
- package/package.json +29 -4
- package/src/function/index.js +109 -0
- package/src/index.js +10 -0
- package/src/monoid/index.js +121 -0
- package/src/option/index.js +321 -0
- package/src/predicate/index.js +72 -0
- package/src/readonly-array/index.js +136 -0
- package/src/result/index.js +455 -0
- package/src/semigroup/index.js +112 -0
- package/src/task/index.js +246 -0
- package/src/task-result/index.js +754 -0
- package/src/validation/index.js +122 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
import * as Task from "../task/index.js";
|
|
2
|
+
import * as Result from "../result/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A task resolving to a Result, with a successful value or an explicit error payload.
|
|
6
|
+
* Call the task without arguments to execute it; calling again reruns its body.
|
|
7
|
+
* An Err is a fulfilled result, distinct from a rejected promise. Unless documented
|
|
8
|
+
* otherwise, helpers propagate rejections instead of converting them into Err.
|
|
9
|
+
* Use fromThrowable to capture thrown or rejected failures as Err values.
|
|
10
|
+
*
|
|
11
|
+
* @template A, E
|
|
12
|
+
* @typedef {() => Promise<import("../result/index.js").Result<A, E>>} TaskResult
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a task resolving to Ok with a captured value.
|
|
18
|
+
* Every run returns the same Ok wrapper. The value is not cloned or awaited.
|
|
19
|
+
*
|
|
20
|
+
* @template A
|
|
21
|
+
* @param {A} value - The successful payload to capture.
|
|
22
|
+
* @returns {TaskResult<A, never>} The task for the captured Ok.
|
|
23
|
+
* @example
|
|
24
|
+
* await ok("Link")(); // { type: "ok", value: "Link" }
|
|
25
|
+
*/
|
|
26
|
+
export const ok = (value) => Task.of(Result.ok(value));
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates a task resolving to Err with a captured error, without rejecting.
|
|
31
|
+
* Every run returns the same Err wrapper.
|
|
32
|
+
*
|
|
33
|
+
* @template E
|
|
34
|
+
* @param {E} error - The failure payload; an Error instance is not required.
|
|
35
|
+
* @returns {TaskResult<never, E>} The task for the captured Err.
|
|
36
|
+
* @example
|
|
37
|
+
* await err("No stamina")(); // { type: "err", error: "No stamina" }
|
|
38
|
+
*/
|
|
39
|
+
export const err = (error) => Task.of(Result.err(error));
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates a task converting null or undefined to Err; other values become Ok.
|
|
44
|
+
* Calls onNull only for a missing value, once per run. Callback throws reject the task.
|
|
45
|
+
*
|
|
46
|
+
* @template E
|
|
47
|
+
* @param {(value: null | undefined) => E} onNull - Receives the original null or undefined.
|
|
48
|
+
* @returns {<A>(value: A) => TaskResult<NonNullable<A>, E>}
|
|
49
|
+
* A function capturing a possibly missing value.
|
|
50
|
+
* @example
|
|
51
|
+
* const check = fromNullable(value => value === null ? "Null" : "Undefined");
|
|
52
|
+
* await check(null)(); // { type: "err", error: "Null" }
|
|
53
|
+
* await check(undefined)(); // { type: "err", error: "Undefined" }
|
|
54
|
+
*/
|
|
55
|
+
export const fromNullable = (onNull) => (value) =>
|
|
56
|
+
Task.defer(() => Result.fromNullable(onNull)(value));
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates a task testing a captured value on each run.
|
|
61
|
+
* Returns Ok when the predicate passes, otherwise Err from onFalse.
|
|
62
|
+
* Callbacks run synchronously; their throws reject the task.
|
|
63
|
+
*
|
|
64
|
+
* @template A, E
|
|
65
|
+
* @param {Object} options - The condition and error factory.
|
|
66
|
+
* @param {(value: A) => boolean} options.predicate - Tests the captured value.
|
|
67
|
+
* @param {(value: A) => E} options.onFalse - Receives the value only when the predicate fails.
|
|
68
|
+
* @returns {(value: A) => TaskResult<A, E>} A function capturing the value to test.
|
|
69
|
+
* @example
|
|
70
|
+
* const check = fromPredicate({ predicate: n => n > 0, onFalse: () => "No stamina" });
|
|
71
|
+
* await check(0)(); // { type: "err", error: "No stamina" }
|
|
72
|
+
*/
|
|
73
|
+
export const fromPredicate = ({ predicate, onFalse }) => (value) =>
|
|
74
|
+
Task.defer(() => Result.fromPredicate({ predicate, onFalse })(value));
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Creates a task converting Some to Ok and None to Err on each run.
|
|
79
|
+
* Calls onNone only for None; its throws reject the task. The captured Option and
|
|
80
|
+
* its value are not cloned.
|
|
81
|
+
*
|
|
82
|
+
* @template E
|
|
83
|
+
* @param {() => E} onNone - Supplies the error payload for None.
|
|
84
|
+
* @returns {<A>(option: import("../option/index.js").Option<A>) => TaskResult<A, E>}
|
|
85
|
+
* A function capturing an Option.
|
|
86
|
+
* @example
|
|
87
|
+
* await fromOption(() => "Hero missing")(Option.none)(); // { type: "err", error: "Hero missing" }
|
|
88
|
+
*/
|
|
89
|
+
export const fromOption = (onNone) => (option) => async ()=>
|
|
90
|
+
Result.fromOption(onNone)(option);
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Creates a task returning the same captured Result on every run.
|
|
95
|
+
* The Result's value or error is not inspected, cloned, or awaited.
|
|
96
|
+
*
|
|
97
|
+
* @template A, E
|
|
98
|
+
* @param {import("../result/index.js").Result<A, E>} result - The Result to capture.
|
|
99
|
+
* @returns {TaskResult<A, E>} The task for the captured Result.
|
|
100
|
+
* @example
|
|
101
|
+
* await fromResult(Result.ok("Link"))(); // { type: "ok", value: "Link" }
|
|
102
|
+
*/
|
|
103
|
+
export const fromResult = (result) => Task.of(result);
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Wraps a task's fulfilled value in Ok on each execution.
|
|
109
|
+
* Rejections propagate; they are not converted to Err. A source throw before it
|
|
110
|
+
* returns a promise throws directly when the resulting task is called.
|
|
111
|
+
*
|
|
112
|
+
* @template A
|
|
113
|
+
* @param {import("../task/index.js").Task<A>} task - The task to execute and wrap.
|
|
114
|
+
* @returns {TaskResult<A, never>} The task wrapping the source value in Ok.
|
|
115
|
+
* @example
|
|
116
|
+
* await fromTask(Task.of("Link"))(); // { type: "ok", value: "Link" }
|
|
117
|
+
*/
|
|
118
|
+
export const fromTask = (task) => Task.map(Result.ok)(task);
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Defers a function call and captures its outcome as a Result.
|
|
123
|
+
* Awaits returned promises or thenables. Synchronous throws and rejections become
|
|
124
|
+
* Err with the original failure payload. Calls the function again on every execution.
|
|
125
|
+
* A returned Result is wrapped in Ok as data, rather than flattened.
|
|
126
|
+
*
|
|
127
|
+
* @template A
|
|
128
|
+
* @param {() => A | PromiseLike<A>} func - Called without arguments on each execution.
|
|
129
|
+
* @returns {TaskResult<A, unknown>} The task capturing success or failure.
|
|
130
|
+
* @example
|
|
131
|
+
* await fromThrowable(async () => "Link")(); // { type: "ok", value: "Link" }
|
|
132
|
+
* await fromThrowable(() => { throw new Error("Offline"); })();
|
|
133
|
+
* // { type: "err", error: new Error("Offline") }
|
|
134
|
+
*/
|
|
135
|
+
export const fromThrowable = (func) => async() => {
|
|
136
|
+
try {
|
|
137
|
+
return Result.ok(await func());
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return Result.err(error);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Transforms Ok on each run; preserves Err. Stores the output without awaiting it.
|
|
146
|
+
* A callback throw rejects the task; a source throw before returning a promise
|
|
147
|
+
* throws directly.
|
|
148
|
+
*
|
|
149
|
+
* @template A, B
|
|
150
|
+
* @param {(value: A) => B} transform - Receives the Ok payload only.
|
|
151
|
+
* @returns {<E>(task: TaskResult<A, E>) => TaskResult<B, E>} A function mapping Ok values.
|
|
152
|
+
* @example
|
|
153
|
+
* await map(health => health + 1)(ok(3))(); // { type: "ok", value: 4 }
|
|
154
|
+
* const result = await map(async health => health + 1)(ok(3))();
|
|
155
|
+
* result.type; // "ok"
|
|
156
|
+
* result.value instanceof Promise; // true
|
|
157
|
+
* await result.value; // 4
|
|
158
|
+
*/
|
|
159
|
+
export const map = (transform) => (task) =>
|
|
160
|
+
Task.map(Result.map(transform))(task);
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Transforms Err on each run; preserves Ok. Stores the output without awaiting it.
|
|
165
|
+
* Source rejections skip transform. A callback throw rejects the task; a source throw
|
|
166
|
+
* before returning a promise throws directly.
|
|
167
|
+
*
|
|
168
|
+
* @template E, F
|
|
169
|
+
* @param {(error: E) => F} transform - Receives the Err payload only.
|
|
170
|
+
* @returns {<A>(task: TaskResult<A, E>) => TaskResult<A, F>} A function mapping Err values.
|
|
171
|
+
* @example
|
|
172
|
+
* await mapError(message => message.toUpperCase())(err("Offline"))();
|
|
173
|
+
* // { type: "err", error: "OFFLINE" }
|
|
174
|
+
*/
|
|
175
|
+
export const mapError = (transform) => (task) =>
|
|
176
|
+
Task.map(Result.mapError(transform))(task);
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Transforms the matching branch on each run, preserving its tag.
|
|
181
|
+
* Stores the output without awaiting it. Source rejections skip both callbacks.
|
|
182
|
+
* A callback throw rejects the task; a source throw before returning a promise throws directly.
|
|
183
|
+
*
|
|
184
|
+
* @template A, B, E, F
|
|
185
|
+
* @param {Object} options - The transformations for each branch.
|
|
186
|
+
* @param {(value: A) => B} options.onOk - Transforms the successful payload.
|
|
187
|
+
* @param {(error: E) => F} options.onErr - Transforms the error payload.
|
|
188
|
+
* @returns {(task: TaskResult<A, E>) => TaskResult<B, F>} A function mapping either branch.
|
|
189
|
+
* @example
|
|
190
|
+
* const label = mapBoth({ onOk: name => `Hero: ${name}`, onErr: message => `Error: ${message}` });
|
|
191
|
+
* await label(ok("Link"))(); // { type: "ok", value: "Hero: Link" }
|
|
192
|
+
*/
|
|
193
|
+
export const mapBoth = ({ onOk, onErr }) => (task) =>
|
|
194
|
+
Task.map(Result.mapBoth({ onOk, onErr }))(task);
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Chains Ok into another TaskResult and runs it; preserves an existing Err.
|
|
199
|
+
* The callback may return the next task or a promise of it. Returns the next Result,
|
|
200
|
+
* including Err. Callback and next-task throws or rejections reject the task;
|
|
201
|
+
* a source throw before returning a promise throws directly.
|
|
202
|
+
*
|
|
203
|
+
* @template A, B, F
|
|
204
|
+
* @param {(value: A) => TaskResult<B, F> | PromiseLike<TaskResult<B, F>>} transform -
|
|
205
|
+
* Supplies the next task for Ok.
|
|
206
|
+
* @returns {<E>(task: TaskResult<A, E>) => TaskResult<B, E | F>} A function chaining TaskResults.
|
|
207
|
+
* @example
|
|
208
|
+
* const heal = flatMap(health => health > 0 ? ok(health + 1) : err("Defeated"));
|
|
209
|
+
* await heal(ok(3))(); // { type: "ok", value: 4 }
|
|
210
|
+
* await heal(ok(0))(); // { type: "err", error: "Defeated" }
|
|
211
|
+
*/
|
|
212
|
+
export const flatMap = (transform) => (task) => {
|
|
213
|
+
const callback = (result) =>
|
|
214
|
+
Result.isOk(result) ? transform(result.value) : Task.of(result);
|
|
215
|
+
|
|
216
|
+
return Task.flatMap(callback)(task)
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Chains Ok into a synchronous Result on each run; preserves an existing Err.
|
|
222
|
+
* A callback throw rejects the task; a source throw before returning a promise throws directly.
|
|
223
|
+
*
|
|
224
|
+
* @template A, B, F
|
|
225
|
+
* @param {(value: A) => import("../result/index.js").Result<B, F>} transform -
|
|
226
|
+
* Supplies the next Result for Ok.
|
|
227
|
+
* @returns {<E>(task: TaskResult<A, E>) => TaskResult<B, E | F>}
|
|
228
|
+
* A function chaining a Result operation.
|
|
229
|
+
* @example
|
|
230
|
+
* const heal = flatMapResult(health => health > 0 ? Result.ok(health + 1) : Result.err("Defeated"));
|
|
231
|
+
* await heal(ok(0))(); // { type: "err", error: "Defeated" }
|
|
232
|
+
*/
|
|
233
|
+
export const flatMapResult = (transform) => (task) =>
|
|
234
|
+
Task.map(Result.flatMap(transform))(task);
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Chains Ok into a synchronous Option on each run; preserves an existing Err.
|
|
239
|
+
* Some becomes Ok; None becomes Err from onNone, which receives the original Ok value.
|
|
240
|
+
* Callback throws reject the task; a source throw before returning a promise throws directly.
|
|
241
|
+
*
|
|
242
|
+
* @template A, B, F
|
|
243
|
+
* @param {Object} options - The Option transformation and missing-value error factory.
|
|
244
|
+
* @param {(value: A) => import("../option/index.js").Option<B>} options.transform -
|
|
245
|
+
* Supplies an Option for Ok.
|
|
246
|
+
* @param {(value: A) => F} options.onNone - Supplies the error when transform returns None.
|
|
247
|
+
* @returns {<E>(task: TaskResult<A, E>) => TaskResult<B, E | F>}
|
|
248
|
+
* A function chaining an Option operation.
|
|
249
|
+
* @example
|
|
250
|
+
* const lookup = flatMapOption({
|
|
251
|
+
* transform: name => name === "Link" ? Option.some("Bow") : Option.none,
|
|
252
|
+
* onNone: name => `No item for ${name}`,
|
|
253
|
+
* });
|
|
254
|
+
* await lookup(ok("Link"))(); // { type: "ok", value: "Bow" }
|
|
255
|
+
* await lookup(ok("Zelda"))(); // { type: "err", error: "No item for Zelda" }
|
|
256
|
+
*/
|
|
257
|
+
export const flatMapOption = ({ transform, onNone }) => (task) =>
|
|
258
|
+
Task.map(Result.flatMapOption({transform, onNone}))(task);
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Runs an outer TaskResult, then the task inside its Ok payload.
|
|
263
|
+
* An outer Err skips the inner task. Otherwise, returns the inner Result, including Err.
|
|
264
|
+
* Rejections propagate. An inner-task throw rejects the returned promise; an
|
|
265
|
+
* outer-task throw before returning a promise throws directly.
|
|
266
|
+
*
|
|
267
|
+
* @template A, E, F
|
|
268
|
+
* @param {TaskResult<TaskResult<A, F>, E>} task - The task containing another task on success.
|
|
269
|
+
* @returns {TaskResult<A, E | F>} The task for the inner Result.
|
|
270
|
+
* @example
|
|
271
|
+
* await flatten(ok(ok("Link")))(); // { type: "ok", value: "Link" }
|
|
272
|
+
* await flatten(ok(err("Offline")))(); // { type: "err", error: "Offline" }
|
|
273
|
+
*/
|
|
274
|
+
export const flatten = (task) => {
|
|
275
|
+
const callback = (result) =>
|
|
276
|
+
Result.isOk(result) ? result.value : Task.of(result);
|
|
277
|
+
|
|
278
|
+
return Task.flatMap(callback)(task);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Keeps a passing Ok or replaces it with Err from onFalse; preserves an existing Err.
|
|
285
|
+
* Callbacks run synchronously during execution. Their throws reject the task;
|
|
286
|
+
* a source throw before returning a promise throws directly.
|
|
287
|
+
*
|
|
288
|
+
* @template A, F
|
|
289
|
+
* @param {Object} options - The success condition and error factory.
|
|
290
|
+
* @param {(value: A) => boolean} options.predicate - Tests the successful payload.
|
|
291
|
+
* @param {(value: A) => F} options.onFalse - Supplies an error when the predicate fails.
|
|
292
|
+
* @returns {<E>(task: TaskResult<A, E>) => TaskResult<A, E | F>} A function filtering Ok values.
|
|
293
|
+
* @example
|
|
294
|
+
* const check = filterOrElse({ predicate: n => n > 0, onFalse: () => "No stamina" });
|
|
295
|
+
* await check(ok(0))(); // { type: "err", error: "No stamina" }
|
|
296
|
+
*/
|
|
297
|
+
export const filterOrElse = ({ predicate, onFalse }) => (task) =>
|
|
298
|
+
Task.map(Result.filterOrElse({ predicate, onFalse }))(task);
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Recovers Err by running a fallback task; preserves Ok. Rejections bypass recovery.
|
|
303
|
+
* The callback may return a task or a promise of it. Returns the fallback Result,
|
|
304
|
+
* including Err. Callback and fallback-task throws or rejections reject the task;
|
|
305
|
+
* a source throw before returning a promise throws directly.
|
|
306
|
+
*
|
|
307
|
+
* @template E, B, F
|
|
308
|
+
* @param {(error: E) => TaskResult<B, F> | PromiseLike<TaskResult<B, F>>} fallback -
|
|
309
|
+
* Supplies a recovery task for Err.
|
|
310
|
+
* @returns {<A>(task: TaskResult<A, E>) => TaskResult<A | B, F>} A function recovering from Err.
|
|
311
|
+
* @example
|
|
312
|
+
* await orElse(() => ok("Guest"))(err("Hero missing"))(); // { type: "ok", value: "Guest" }
|
|
313
|
+
*/
|
|
314
|
+
export const orElse = (fallback) => (task) => {
|
|
315
|
+
const callback = (result) =>
|
|
316
|
+
Result.isErr(result) ? fallback(result.error) : Task.of(result);
|
|
317
|
+
|
|
318
|
+
return Task.flatMap(callback)(task);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Runs an effect for Ok and returns the same Result; skips Err.
|
|
324
|
+
* Ignores the effect's return value without awaiting it; use tapAsync for async effects.
|
|
325
|
+
* An effect throw rejects the task; a source throw before returning a promise throws directly.
|
|
326
|
+
*
|
|
327
|
+
* @template A
|
|
328
|
+
* @param {(value: A) => unknown} effect - Observes the Ok value.
|
|
329
|
+
* @returns {<E>(task: TaskResult<A, E>) => TaskResult<A, E>} A function observing Ok values.
|
|
330
|
+
* @example
|
|
331
|
+
* const seen = [];
|
|
332
|
+
* await tap(value => seen.push(value))(ok("Link"))(); // { type: "ok", value: "Link" }
|
|
333
|
+
* seen; // ["Link"]
|
|
334
|
+
*/
|
|
335
|
+
export const tap = (effect) => (task) => () =>
|
|
336
|
+
task().then(result => {
|
|
337
|
+
if (Result.isOk(result)) {
|
|
338
|
+
effect(result.value);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return result;
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Awaits an effect for Ok and returns the same Result object.
|
|
347
|
+
* Skips the effect for Err. Source or effect throws and rejections reject the task.
|
|
348
|
+
*
|
|
349
|
+
* @template A
|
|
350
|
+
* @param {(value: A) => unknown} effect - Observes the Ok value; may return a promise.
|
|
351
|
+
* @returns {<E>(task: TaskResult<A, E>) => TaskResult<A, E>} A function awaiting an effect for Ok.
|
|
352
|
+
* @example
|
|
353
|
+
* const seen = [];
|
|
354
|
+
* await tapAsync(async value => {
|
|
355
|
+
* await Promise.resolve();
|
|
356
|
+
* seen.push(value);
|
|
357
|
+
* })(ok("Link"))(); // { type: "ok", value: "Link" }
|
|
358
|
+
* seen; // ["Link"]
|
|
359
|
+
*/
|
|
360
|
+
export const tapAsync = (effect) => (task) => async () => {
|
|
361
|
+
const result = await task();
|
|
362
|
+
|
|
363
|
+
if (Result.isOk(result)) {
|
|
364
|
+
await effect(result.value);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Runs an effect for Err and returns the same Result; skips Ok and source rejections.
|
|
373
|
+
* Ignores the effect's return value without awaiting it; use tapErrorAsync for async effects.
|
|
374
|
+
* An effect throw rejects the task; a source throw before returning a promise throws directly.
|
|
375
|
+
*
|
|
376
|
+
* @template E
|
|
377
|
+
* @param {(error: E) => unknown} effect - Observes the Err value.
|
|
378
|
+
* @returns {<A>(task: TaskResult<A, E>) => TaskResult<A, E>} A function observing Err values.
|
|
379
|
+
* @example
|
|
380
|
+
* const seen = [];
|
|
381
|
+
* await tapError(error => seen.push(error))(err("Offline"))(); // { type: "err", error: "Offline" }
|
|
382
|
+
* seen; // ["Offline"]
|
|
383
|
+
*/
|
|
384
|
+
export const tapError = (effect) => (task) => () =>
|
|
385
|
+
task().then(result => {
|
|
386
|
+
if (Result.isErr(result)) {
|
|
387
|
+
effect(result.error);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return result;
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Awaits an effect for Err and returns the same Result object.
|
|
396
|
+
* Skips the effect for Ok and for rejected promises. Source or effect throws and
|
|
397
|
+
* rejections reject the task.
|
|
398
|
+
*
|
|
399
|
+
* @template E
|
|
400
|
+
* @param {(error: E) => unknown} effect - Observes the Err value; may return a promise.
|
|
401
|
+
* @returns {<A>(task: TaskResult<A, E>) => TaskResult<A, E>} A function awaiting an effect for Err.
|
|
402
|
+
* @example
|
|
403
|
+
* const seen = [];
|
|
404
|
+
* await tapErrorAsync(async error => {
|
|
405
|
+
* await Promise.resolve();
|
|
406
|
+
* seen.push(error);
|
|
407
|
+
* })(err("Offline"))(); // { type: "err", error: "Offline" }
|
|
408
|
+
* seen; // ["Offline"]
|
|
409
|
+
*/
|
|
410
|
+
export const tapErrorAsync = (effect) => (task) => async () => {
|
|
411
|
+
const result = await task();
|
|
412
|
+
|
|
413
|
+
if (Result.isErr(result)) {
|
|
414
|
+
await effect(result.error);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Unwraps Ok or calls fallback for Err, producing a plain Task.
|
|
423
|
+
* Awaits the selected value if promise-like. Source rejections skip fallback.
|
|
424
|
+
* A fallback throw rejects the task; a source throw before returning a promise throws directly.
|
|
425
|
+
*
|
|
426
|
+
* @template E, B
|
|
427
|
+
* @param {(error: E) => B} fallback - Receives the Err payload and returns a value or promise.
|
|
428
|
+
* @returns {<A>(task: TaskResult<A, E>) => import("../task/index.js").Task<Awaited<A | B>>}
|
|
429
|
+
* A function unwrapping a TaskResult.
|
|
430
|
+
* @example
|
|
431
|
+
* await getOrElse(() => "Guest")(err("Hero missing"))(); // "Guest"
|
|
432
|
+
*/
|
|
433
|
+
export const getOrElse = (fallback) => (task) =>
|
|
434
|
+
Task.map(Result.getOrElse(fallback))(task);
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Calls the matching handler, producing a plain Task that awaits its output.
|
|
439
|
+
* Source rejections skip both handlers. A callback throw rejects the task;
|
|
440
|
+
* a source throw before returning a promise throws directly.
|
|
441
|
+
*
|
|
442
|
+
* @template A, B, E, C
|
|
443
|
+
* @param {Object} options - The handlers for each Result branch.
|
|
444
|
+
* @param {(value: A) => B} options.onOk - Handles the Ok value; may return a promise.
|
|
445
|
+
* @param {(error: E) => C} options.onErr - Handles the Err value; may return a promise.
|
|
446
|
+
* @returns {(task: TaskResult<A, E>) => import("../task/index.js").Task<Awaited<B | C>>}
|
|
447
|
+
* A function handling either branch.
|
|
448
|
+
* @example
|
|
449
|
+
* const describe = match({ onOk: name => `Hello, ${name}`, onErr: error => `Error: ${error}` });
|
|
450
|
+
* await describe(ok("Link"))(); // "Hello, Link"
|
|
451
|
+
*/
|
|
452
|
+
export const match = ({ onOk, onErr }) => (task) =>
|
|
453
|
+
Task.map(Result.match({ onOk, onErr }))(task);
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Runs tasks sequentially, collecting Ok values in iteration order.
|
|
458
|
+
* Stops at the first Err and returns it unchanged. Throws or rejections also stop
|
|
459
|
+
* execution and reject the task. Empty input resolves to Ok([]).
|
|
460
|
+
* Each run traverses the iterable again; iterators may be single-use.
|
|
461
|
+
*
|
|
462
|
+
* @template A, E
|
|
463
|
+
* @param {Iterable<TaskResult<A, E>>} tasks - Tasks to execute in iteration order.
|
|
464
|
+
* @returns {TaskResult<A[], E>} The task collecting sequential results.
|
|
465
|
+
* @example
|
|
466
|
+
* const visited = [];
|
|
467
|
+
* const later = async () => { visited.push("Zelda"); return Result.ok("Zelda"); };
|
|
468
|
+
* await all([err("Offline"), later])(); // { type: "err", error: "Offline" }
|
|
469
|
+
* visited; // []
|
|
470
|
+
*/
|
|
471
|
+
export const all = (tasks) => async () => {
|
|
472
|
+
const outputs = [];
|
|
473
|
+
|
|
474
|
+
for (const task of tasks) {
|
|
475
|
+
const result = await task();
|
|
476
|
+
|
|
477
|
+
if (Result.isErr(result)) {
|
|
478
|
+
return result;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
outputs.push(result.value);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return Result.ok(outputs);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Starts tasks concurrently and collects Ok values in input order.
|
|
490
|
+
* Err does not stop work. After all fulfill, returns the first Err in input order,
|
|
491
|
+
* or Ok with all values. Empty input resolves to Ok([]). Repeats tasks on each run.
|
|
492
|
+
* Rejections do not cancel running tasks. A synchronous source throw rejects the
|
|
493
|
+
* combined task and prevents later tasks from starting.
|
|
494
|
+
*
|
|
495
|
+
* @template A, E
|
|
496
|
+
* @param {ReadonlyArray<TaskResult<A, E>>} tasks - Tasks to run; the array must have no holes.
|
|
497
|
+
* @returns {TaskResult<A[], E>} The task collecting parallel results.
|
|
498
|
+
* @example
|
|
499
|
+
* const visited = [];
|
|
500
|
+
* const later = async () => { visited.push("Zelda"); return Result.ok("Zelda"); };
|
|
501
|
+
* await allParallel([err("Offline"), later])(); // { type: "err", error: "Offline" }
|
|
502
|
+
* visited; // ["Zelda"]
|
|
503
|
+
*/
|
|
504
|
+
export const allParallel = (tasks) =>
|
|
505
|
+
Task.map(Result.all)(Task.all(tasks));
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Runs at most concurrency tasks at once, collecting values in input order.
|
|
510
|
+
* Err does not stop queued work, even with concurrency 1. After all fulfill, returns
|
|
511
|
+
* the first Err in input order, or Ok with all values. Empty input resolves to Ok([]).
|
|
512
|
+
* A throw or rejection rejects the task and stops queued work once observed;
|
|
513
|
+
* running tasks are not cancelled. Scheduling resets each run.
|
|
514
|
+
*
|
|
515
|
+
* @param {number} concurrency - A positive safe integer, validated immediately.
|
|
516
|
+
* @returns {<A, E>(tasks: ReadonlyArray<TaskResult<A, E>>) => TaskResult<A[], E>}
|
|
517
|
+
* A function collecting tasks with limited concurrency.
|
|
518
|
+
* @throws {RangeError} If concurrency is not a positive safe integer.
|
|
519
|
+
* @example
|
|
520
|
+
* const visited = [];
|
|
521
|
+
* const later = async () => { visited.push("Zelda"); return Result.ok("Zelda"); };
|
|
522
|
+
* await allParallelLimit(1)([err("Offline"), later])(); // { type: "err", error: "Offline" }
|
|
523
|
+
* visited; // ["Zelda"]
|
|
524
|
+
*/
|
|
525
|
+
export const allParallelLimit = (concurrency) =>
|
|
526
|
+
traverseParallelLimit({ concurrency, transform: task => task });
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Creates and runs tasks sequentially, collecting Ok values in iteration order.
|
|
531
|
+
* Stops at the first Err without transforming later elements. Throws or rejections
|
|
532
|
+
* stop iteration and reject the task. Empty input resolves to Ok([]).
|
|
533
|
+
* Each run traverses the iterable again; iterators may be single-use.
|
|
534
|
+
*
|
|
535
|
+
* @template A, B, E
|
|
536
|
+
* @param {(value: A) => TaskResult<B, E>} transform - Creates each task during execution.
|
|
537
|
+
* @returns {(values: Iterable<A>) => TaskResult<B[], E>}
|
|
538
|
+
* A function collecting sequential task results.
|
|
539
|
+
* @example
|
|
540
|
+
* const visited = [];
|
|
541
|
+
* const load = traverse(name => {
|
|
542
|
+
* visited.push(name);
|
|
543
|
+
* return name === "Link" ? err("Offline") : ok(name);
|
|
544
|
+
* });
|
|
545
|
+
* await load(["Link", "Zelda"])(); // { type: "err", error: "Offline" }
|
|
546
|
+
* visited; // ["Link"]
|
|
547
|
+
*/
|
|
548
|
+
export const traverse = (transform) => (values) => async () => {
|
|
549
|
+
const outputs = [];
|
|
550
|
+
|
|
551
|
+
for (const value of values) {
|
|
552
|
+
const task = transform(value)
|
|
553
|
+
const result = await task()
|
|
554
|
+
|
|
555
|
+
if (Result.isErr(result)) {
|
|
556
|
+
return result;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
outputs.push(result.value);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return Result.ok(outputs);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Creates and runs tasks concurrently, collecting Ok values in input order.
|
|
568
|
+
* Err does not stop work. After all fulfill, returns the first Err in input order,
|
|
569
|
+
* or Ok with all values. Empty input resolves to Ok([]). Repeats transforms and tasks each run.
|
|
570
|
+
* Rejections do not cancel running tasks. A synchronous transform or task throw
|
|
571
|
+
* rejects the combined task and skips later elements. Supply an array without holes:
|
|
572
|
+
* skipped elements leave undefined Results, which can cause a TypeError during collection.
|
|
573
|
+
*
|
|
574
|
+
* @template A, B, E
|
|
575
|
+
* @param {(value: A) => TaskResult<B, E>} transform - Creates a task from each element in input order.
|
|
576
|
+
* @returns {(values: ReadonlyArray<A>) => TaskResult<B[], E>}
|
|
577
|
+
* A function collecting parallel task results.
|
|
578
|
+
* @example
|
|
579
|
+
* const visited = [];
|
|
580
|
+
* const load = traverseParallel(name => {
|
|
581
|
+
* visited.push(name);
|
|
582
|
+
* return name === "Link" ? err("Offline") : ok(name);
|
|
583
|
+
* });
|
|
584
|
+
* await load(["Link", "Zelda"])(); // { type: "err", error: "Offline" }
|
|
585
|
+
* visited; // ["Link", "Zelda"]
|
|
586
|
+
*/
|
|
587
|
+
export const traverseParallel = (transform) => (values) =>
|
|
588
|
+
Task.map(Result.all)(Task.traverse(transform)(values));
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Creates and runs at most concurrency tasks at once, starting the next when a slot opens.
|
|
593
|
+
* Err does not stop queued work. After all fulfill, returns the first Err in input order,
|
|
594
|
+
* or Ok with values in input order. Empty input resolves to Ok([]).
|
|
595
|
+
* Throws or rejections reject the task and stop queued work once observed, without
|
|
596
|
+
* cancelling running tasks. Scheduling resets each run.
|
|
597
|
+
*
|
|
598
|
+
* @template A, B, E
|
|
599
|
+
* @param {Object} options - The concurrency limit and task factory.
|
|
600
|
+
* @param {number} options.concurrency - A positive safe integer, validated immediately.
|
|
601
|
+
* @param {(value: A) => TaskResult<B, E>} options.transform - Creates a task when a slot opens.
|
|
602
|
+
* @returns {(values: ReadonlyArray<A>) => TaskResult<B[], E>}
|
|
603
|
+
* A function collecting tasks with limited concurrency.
|
|
604
|
+
* @throws {RangeError} If concurrency is not a positive safe integer.
|
|
605
|
+
* @example
|
|
606
|
+
* let active = 0;
|
|
607
|
+
* let peak = 0;
|
|
608
|
+
* const load = traverseParallelLimit({
|
|
609
|
+
* concurrency: 2,
|
|
610
|
+
* transform: name => async () => {
|
|
611
|
+
* peak = Math.max(peak, ++active);
|
|
612
|
+
* await Promise.resolve();
|
|
613
|
+
* active--;
|
|
614
|
+
* return Result.ok(name);
|
|
615
|
+
* },
|
|
616
|
+
* });
|
|
617
|
+
* await load(["Link", "Zelda", "Epona"])(); // { type: "ok", value: ["Link", "Zelda", "Epona"] }
|
|
618
|
+
* peak; // 2
|
|
619
|
+
*/
|
|
620
|
+
export const traverseParallelLimit = ({ concurrency, transform }) => {
|
|
621
|
+
if (!Number.isSafeInteger(concurrency) || concurrency <= 0) {
|
|
622
|
+
throw new RangeError('concurrency must be a positive safe integer');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return (values) => async () => {
|
|
626
|
+
const results = new Array(values.length);
|
|
627
|
+
|
|
628
|
+
let index = 0;
|
|
629
|
+
let stopped = false;
|
|
630
|
+
|
|
631
|
+
const worker = async () => {
|
|
632
|
+
while (!stopped && index < values.length) {
|
|
633
|
+
const currentIndex = index;
|
|
634
|
+
index ++;
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
results[currentIndex] = await transform(values[currentIndex])();
|
|
638
|
+
} catch (error) {
|
|
639
|
+
stopped = true;
|
|
640
|
+
throw error;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const workers = []
|
|
646
|
+
const parallel = Math.min(concurrency, values.length);
|
|
647
|
+
|
|
648
|
+
for (let count = 0; count < parallel; count++) {
|
|
649
|
+
workers.push(worker());
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
await Promise.all(workers);
|
|
653
|
+
|
|
654
|
+
return Result.all(results);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Runs and awaits cleanup after the source settles, including after a throw or rejection.
|
|
661
|
+
* If cleanup succeeds, preserves the original Result or rejection. If cleanup fails
|
|
662
|
+
* after Ok, rejects with the cleanup failure. After Err or a source failure, rejects
|
|
663
|
+
* with AggregateError whose errors array contains [original error, cleanup error].
|
|
664
|
+
* The original error is the Err payload or the source's thrown value or rejection reason.
|
|
665
|
+
* Cleanup runs once on every execution; its failures are not converted into Err.
|
|
666
|
+
*
|
|
667
|
+
* @param {() => unknown} cleanup - Cleanup to run and await without arguments.
|
|
668
|
+
* @returns {<A, E>(task: TaskResult<A, E>) => TaskResult<A, E>} A function attaching cleanup.
|
|
669
|
+
* @example
|
|
670
|
+
* const events = [];
|
|
671
|
+
* const load = withCleanup(async () => {
|
|
672
|
+
* await Promise.resolve();
|
|
673
|
+
* events.push("cleaned");
|
|
674
|
+
* })(err("Offline"));
|
|
675
|
+
* await load(); // { type: "err", error: "Offline" }
|
|
676
|
+
* events; // ["cleaned"]
|
|
677
|
+
*/
|
|
678
|
+
export const withCleanup = (cleanup) => (task) => async () => {
|
|
679
|
+
let result;
|
|
680
|
+
|
|
681
|
+
let taskError;
|
|
682
|
+
let taskFailed = false;
|
|
683
|
+
|
|
684
|
+
try {
|
|
685
|
+
result = await task();
|
|
686
|
+
} catch (error) {
|
|
687
|
+
taskError = error;
|
|
688
|
+
taskFailed = true;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
try {
|
|
692
|
+
await cleanup();
|
|
693
|
+
} catch (error) {
|
|
694
|
+
if (taskFailed) {
|
|
695
|
+
throw new AggregateError([taskError, error]);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (Result.isErr(result)) {
|
|
699
|
+
throw new AggregateError([result.error, error]);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
throw error;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (taskFailed) {
|
|
706
|
+
throw taskError;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return result;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Retries an Err when the retry policy allows it, waiting between attempts.
|
|
716
|
+
* The attempt limit includes the first run. Callbacks receive the failed attempt's
|
|
717
|
+
* number, starting at 1, and are skipped for Ok or the final attempt. Calls delay only
|
|
718
|
+
* if retryOn permits another attempt. Throws and rejections reject the task without
|
|
719
|
+
* retrying. The attempt count resets on each execution.
|
|
720
|
+
*
|
|
721
|
+
* @template E
|
|
722
|
+
* @param {Object} options - The retry limit, policy, and delay calculation.
|
|
723
|
+
* @param {number} options.attempts - A positive safe integer, validated immediately.
|
|
724
|
+
* @param {(error: E, attempt: number) => boolean} options.retryOn -
|
|
725
|
+
* Whether to retry the error from this attempt.
|
|
726
|
+
* @param {(error: E, attempt: number) => number} options.delay -
|
|
727
|
+
* Delay in milliseconds: an integer from 0 to 2147483647. An invalid delay rejects the task with
|
|
728
|
+
* RangeError when a retry is scheduled.
|
|
729
|
+
* @returns {<A>(task: TaskResult<A, E>) => TaskResult<A, E>} A function retrying Err results.
|
|
730
|
+
* @throws {RangeError} If attempts is not a positive safe integer.
|
|
731
|
+
* @example
|
|
732
|
+
* let calls = 0;
|
|
733
|
+
* const source = async () => ++calls < 2 ? Result.err("Offline") : Result.ok("Link");
|
|
734
|
+
* const load = retry({ attempts: 3, retryOn: error => error === "Offline", delay: () => 0 })(source);
|
|
735
|
+
* await load(); // { type: "ok", value: "Link" }
|
|
736
|
+
* calls; // 2
|
|
737
|
+
*/
|
|
738
|
+
export const retry = ({ attempts, retryOn, delay }) => {
|
|
739
|
+
if (!Number.isSafeInteger(attempts) || attempts < 1) {
|
|
740
|
+
throw new RangeError('attempts must be a positive integer');
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return (task) => async () => {
|
|
744
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
745
|
+
const result = await task();
|
|
746
|
+
|
|
747
|
+
if (Result.isOk(result) || attempt === attempts || !retryOn(result.error, attempt)) {
|
|
748
|
+
return result;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
await Task.sleep(delay(result.error, attempt))();
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|