@nlozgachev/pipelined 0.27.0 → 0.28.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/README.md CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@nlozgachev/pipelined?style=for-the-badge&color=000&logo=npm&label&logoColor=fff)](https://www.npmjs.com/package/@nlozgachev/pipelined) [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/nlozgachev/pipelined/publish.yml?style=for-the-badge&color=000&logo=githubactions&label&logoColor=fff)](https://github.com/nlozgachev/pipelined/actions/workflows/publish.yml) [![Codecov](https://img.shields.io/codecov/c/github/nlozgachev/pipelined?style=for-the-badge&color=000&logo=codecov&label&logoColor=fff)](https://app.codecov.io/github/nlozgachev/pipelined)
4
4
 
5
-
6
5
  Opinionated functional abstractions for TypeScript.
7
6
 
8
7
  > **Note:** pipelined is pre-1.0. The API may change between minor versions until the 1.0 release.
@@ -14,49 +13,165 @@ npm add @nlozgachev/pipelined
14
13
  ## Possibly maybe
15
14
 
16
15
  **pipelined** names every possible state and gives you operations that compose. `Maybe<A>` for values
17
- that may or may not be there. `Result<E, A>` for operations that succeed or fail
18
- with a typed error. `TaskResult<E, A>` for async operations that do both lazily, with retry,
19
- timeout, and cancellation built in. And, of course, there is more than that.
16
+ that may or may not be there. `Result<E, A>` for operations that succeed or fail with a typed error.
17
+ `TaskResult<E, A>` for async operations that keep failures as typed values and propagate cancellation
18
+ automatically. `Op<I, E, A>` for managing repeated async interactions retry, timeout, and
19
+ concurrency strategy in one place. And, of course, there is more than that.
20
20
 
21
21
  ## Documentation
22
22
 
23
23
  Full guides and API reference at **[pipelined.lozgachev.dev](https://pipelined.lozgachev.dev)**.
24
24
 
25
- ## Example: a single async call
25
+ ## Example: composing optional values
26
+
27
+ `null` checks accumulate fast. Each one is a conditional branch that the type system can't help you
28
+ forget. `Maybe<A>` turns absence into a value that composes — the same operations apply whether or
29
+ not anything is there:
30
+
31
+ ```ts
32
+ import { pipe } from "@nlozgachev/pipelined/composition";
33
+ import { Maybe } from "@nlozgachev/pipelined/core";
34
+ import { Num, Str } from "@nlozgachev/pipelined/utils";
35
+
36
+ const parseDiscount = (raw: string): string =>
37
+ pipe(
38
+ raw,
39
+ Str.trim,
40
+ Num.parse, // "10" → Some(10), "abc" → None
41
+ Maybe.filter((n) => n >= 0 && n <= 100), // out of range → None
42
+ Maybe.map((n) => `${n}% off`),
43
+ Maybe.getOrElse(() => "No discount"),
44
+ );
45
+
46
+ parseDiscount(" 15 "); // "15% off"
47
+ parseDiscount("150"); // "No discount"
48
+ parseDiscount("abc"); // "No discount"
49
+ ```
50
+
51
+ Every step that sees `None` is skipped. The fallback runs once, at the end.
52
+
53
+ ## Example: typed async errors
54
+
55
+ Unhandled rejections are invisible until they crash. `TaskResult<E, A>` keeps failures as typed
56
+ values — the error type is part of the signature, not a runtime surprise.
57
+
58
+ ```ts
59
+ import { pipe } from "@nlozgachev/pipelined/composition";
60
+ import { Result, TaskResult } from "@nlozgachev/pipelined/core";
61
+
62
+ type ApiError = { status: number; message: string };
63
+
64
+ const fetchUser = (id: string): TaskResult<ApiError, User> =>
65
+ TaskResult.tryCatch(
66
+ (signal) =>
67
+ fetch(`/users/${id}`, { signal }).then((r) => {
68
+ if (!r.ok) throw { status: r.status, message: r.statusText };
69
+ return r.json() as Promise<User>;
70
+ }),
71
+ (e) => e as ApiError,
72
+ );
73
+
74
+ const fetchPosts = (userId: string): TaskResult<ApiError, Post[]> =>
75
+ TaskResult.tryCatch(
76
+ (signal) => fetch(`/users/${userId}/posts`, { signal }).then((r) => r.json()),
77
+ (e) => e as ApiError,
78
+ );
79
+
80
+ // Chain two requests — the AbortSignal propagates to both automatically
81
+ const userWithPosts = (id: string) =>
82
+ pipe(
83
+ fetchUser(id),
84
+ TaskResult.chain((user) =>
85
+ pipe(
86
+ fetchPosts(user.id),
87
+ TaskResult.map((posts) => ({ ...user, posts })),
88
+ )
89
+ ),
90
+ );
91
+ ```
92
+
93
+ `userWithPosts` is a lazy function — nothing runs until called. The `AbortSignal` threads through
94
+ both requests: abort at any point and whichever request is in flight is cancelled immediately.
95
+
96
+ ```ts
97
+ const controller = new AbortController();
98
+ const result = await userWithPosts("42")(controller.signal);
99
+
100
+ if (Result.isOk(result)) {
101
+ render(result.value); // { ...User, posts: Post[] }
102
+ } else {
103
+ showError(result.error); // ApiError — typed, not unknown
104
+ }
105
+ ```
106
+
107
+ ## Example: transforming data
108
+
109
+ The utils modules wrap JavaScript's built-in types with data-last, curried operations that return
110
+ `Maybe` wherever a value might be absent. They compose naturally with the core types:
111
+
112
+ ```ts
113
+ import { pipe } from "@nlozgachev/pipelined/composition";
114
+ import { Maybe } from "@nlozgachev/pipelined/core";
115
+ import { Arr, Num, Rec, Str } from "@nlozgachev/pipelined/utils";
116
+
117
+ type RawItem = { name: string; price: string; category: string };
118
+ type Item = { name: string; price: number; category: string };
119
+
120
+ const normalise = (raw: RawItem): Maybe<Item> =>
121
+ pipe(
122
+ Num.parse(raw.price), // "9.99" → Some(9.99), "n/a" → None
123
+ Maybe.map((price) => ({ name: Str.trim(raw.name), price, category: raw.category })),
124
+ );
125
+
126
+ const cheapestByCategory = (items: RawItem[]) =>
127
+ pipe(
128
+ items,
129
+ Arr.filterMap(normalise), // parse + drop unparseable prices in one pass
130
+ Arr.sortBy((a, b) => a.price - b.price), // ascending price
131
+ Arr.groupBy((item) => item.category), // Record<string, NonEmptyList<Item>>
132
+ Rec.map((group) => Arr.head(group)), // cheapest per category — Maybe<Item>
133
+ );
134
+ ```
135
+
136
+ `filterMap` applies a function that returns `Maybe` and collects only the `Some` results — one step
137
+ replaces a `map` followed by a `filter`. `Arr.head` returns `Maybe<Item>` rather than `Item | undefined`,
138
+ so the absence is explicit in the type and the rest of the pipeline handles it the same way.
139
+
140
+ ## Example: retry, timeout, and cancellation
26
141
 
27
142
  A careful, production-minded attempt at "fetch with retry, timeout, and cancellation":
28
143
 
29
144
  ```ts
30
145
  type UserResult =
31
- | { ok: true; user: User; }
32
- | { ok: false; error: "Timeout" | "NetworkError"; };
146
+ | { ok: true; user: User; }
147
+ | { ok: false; error: "Timeout" | "NetworkError"; };
33
148
 
34
149
  async function fetchUser(
35
- id: string,
36
- signal?: AbortSignal,
150
+ id: string,
151
+ signal?: AbortSignal,
37
152
  ): Promise<UserResult> {
38
- async function attempt(n: number): Promise<UserResult> {
39
- const controller = new AbortController();
40
- const timerId = setTimeout(() => controller.abort(), 5000);
41
- signal?.addEventListener("abort", () => controller.abort(), { once: true });
42
-
43
- try {
44
- const res = await fetch(`/users/${id}`, { signal: controller.signal });
45
- clearTimeout(timerId);
46
- return { ok: true, user: await res.json() };
47
- } catch (e) {
48
- clearTimeout(timerId);
49
- if ((e as Error).name === "AbortError" && !signal?.aborted) {
50
- return { ok: false, error: "Timeout" };
51
- }
52
- if (n < 3) {
53
- await new Promise((r) => setTimeout(r, n * 1000));
54
- return attempt(n + 1);
55
- }
56
- return { ok: false, error: "NetworkError" };
57
- }
58
- }
59
- return attempt(1);
153
+ async function attempt(n: number): Promise<UserResult> {
154
+ const controller = new AbortController();
155
+ const timerId = setTimeout(() => controller.abort(), 5000);
156
+ signal?.addEventListener("abort", () => controller.abort(), { once: true });
157
+
158
+ try {
159
+ const res = await fetch(`/users/${id}`, { signal: controller.signal });
160
+ clearTimeout(timerId);
161
+ return { ok: true, user: await res.json() };
162
+ } catch (e) {
163
+ clearTimeout(timerId);
164
+ if ((e as Error).name === "AbortError" && !signal?.aborted) {
165
+ return { ok: false, error: "Timeout" };
166
+ }
167
+ if (n < 3) {
168
+ await new Promise((r) => setTimeout(r, n * 1000));
169
+ return attempt(n + 1);
170
+ }
171
+ return { ok: false, error: "NetworkError" };
172
+ }
173
+ }
174
+ return attempt(1);
60
175
  }
61
176
  ```
62
177
 
@@ -67,38 +182,43 @@ to thread the attempt count.
67
182
  With **pipelined**:
68
183
 
69
184
  ```ts
70
- import { pipe } from "@nlozgachev/pipelined/composition";
71
- import { Result, TaskResult } from "@nlozgachev/pipelined/core";
185
+ import { Op } from "@nlozgachev/pipelined/core";
72
186
 
73
- const fetchUser = (id: string): TaskResult<ApiError, User> =>
74
- pipe(
75
- TaskResult.tryCatch(
76
- (signal) => fetch(`/users/${id}`, { signal }).then((r) => r.json()),
77
- (e) => new ApiError(e),
78
- ),
79
- TaskResult.timeout(5000, () => new ApiError("request timed out")),
80
- TaskResult.retry({ attempts: 3, backoff: (n) => n * 1000 }),
81
- );
187
+ const fetchUser = Op.interpret(
188
+ Op.create(
189
+ (signal) => (id: string) => fetch(`/users/${id}`, { signal }).then((r) => r.json() as Promise<User>),
190
+ (e) => new ApiError(e),
191
+ ),
192
+ {
193
+ strategy: "restartable",
194
+ retry: { attempts: 3, backoff: (n) => n * 1000 },
195
+ timeout: { ms: 5000, onTimeout: () => new ApiError("request timed out") },
196
+ },
197
+ );
82
198
  ```
83
199
 
84
- `TaskResult<ApiError, User>` is a lazy function — nothing runs until called. The `AbortSignal`
85
- threads through every retry and the timeout automatically. The return type is the contract:
86
- `ApiError` on the left, `User` on the right, nothing escapes as an exception.
200
+ `fetchUser` is a managed operator — nothing runs until you call `run`. Retry logic, signal
201
+ propagation, and timeout wiring are handled automatically. The outcome type is the contract:
202
+ `ApiError` on the left, `User` on the right, nothing escapes as an unhandled exception.
87
203
 
88
204
  ```ts
89
- const controller = new AbortController();
90
- const result = await fetchUser("42")(controller.signal);
205
+ const outcome = await fetchUser.run("42");
91
206
 
92
- if (Result.isOk(result)) {
93
- render(result.value); // User
94
- } else {
95
- showError(result.error); // ApiError, not unknown
207
+ if (Op.isOk(outcome)) {
208
+ render(outcome.value); // User
209
+ } else if (Op.isErr(outcome)) {
210
+ showError(outcome.error); // ApiError, not unknown
96
211
  }
212
+
213
+ // explicit cancellation — in-flight request is aborted immediately
214
+ fetchUser.abort();
97
215
  ```
98
216
 
99
217
  ## Example: repeated interactions
100
218
 
101
- `TaskResult` handles one call well. Real UIs make the same call many times — a search input fires on every keystroke, a submit button gets clicked twice, a polling loop needs to stop when something newer starts. Each scenario has a different answer to the same question: *what happens to the previous call when a new one arrives?*
219
+ Real UIs make the same call many times — a search input fires on every keystroke, a submit button
220
+ gets clicked twice, a polling loop needs to stop when something newer starts. Each scenario has a
221
+ different answer to the same question: _what happens to the previous call when a new one arrives?_
102
222
 
103
223
  `Op` makes that question a one-word configuration choice.
104
224
 
@@ -108,21 +228,21 @@ if (Result.isOk(result)) {
108
228
  import { Op } from "@nlozgachev/pipelined/core";
109
229
 
110
230
  const searchOp = Op.create(
111
- (signal) => (query: string) =>
112
- fetch(`/search?q=${query}`, { signal }).then((r) => r.json() as Promise<SearchResult[]>),
113
- (e) => new SearchError(e),
231
+ (signal) => (query: string) =>
232
+ fetch(`/search?q=${query}`, { signal }).then((r) => r.json() as Promise<SearchResult[]>),
233
+ (e) => new SearchError(e),
114
234
  );
115
235
 
116
236
  const search = Op.interpret(searchOp, {
117
- strategy: "restartable", // new call cancels the previous one
118
- retry: { attempts: 2, backoff: 300 },
237
+ strategy: "restartable", // new call cancels the previous one
238
+ retry: { attempts: 2, backoff: 300 },
119
239
  });
120
240
 
121
241
  search.subscribe((state) => {
122
- if (state.kind === "Pending") showSpinner();
123
- if (state.kind === "Retrying") showSpinner(`retrying… attempt ${state.attempt}`);
124
- if (state.kind === "Ok") showResults(state.value);
125
- if (state.kind === "Err") showError(state.error);
242
+ if (state.kind === "Pending") showSpinner();
243
+ if (state.kind === "Retrying") showSpinner(`retrying… attempt ${state.attempt}`);
244
+ if (state.kind === "Ok") showResults(state.value);
245
+ if (state.kind === "Err") showError(state.error);
126
246
  });
127
247
 
128
248
  input.addEventListener("input", (e) => search.run(e.currentTarget.value));
@@ -132,28 +252,27 @@ input.addEventListener("input", (e) => search.run(e.currentTarget.value));
132
252
 
133
253
  ```ts
134
254
  const submitOp = Op.create(
135
- (signal) => (data: FormData) =>
136
- fetch("/orders", { method: "POST", body: data, signal }).then((r) => r.json()),
137
- (e) => new ApiError(e),
255
+ (signal) => (data: FormData) => fetch("/orders", { method: "POST", body: data, signal }).then((r) => r.json()),
256
+ (e) => new ApiError(e),
138
257
  );
139
258
 
140
259
  const submit = Op.interpret(submitOp, {
141
- strategy: "exclusive", // in-flight? new calls are dropped immediately
260
+ strategy: "exclusive", // in-flight? new calls are dropped immediately
142
261
  });
143
262
 
144
263
  submit.subscribe((state) => {
145
- submitButton.disabled = state.kind === "Pending";
146
- if (state.kind === "Ok") showConfirmation(state.value);
147
- if (state.kind === "Err") showError(state.error);
264
+ submitButton.disabled = state.kind === "Pending";
265
+ if (state.kind === "Ok") showConfirmation(state.value);
266
+ if (state.kind === "Err") showError(state.error);
148
267
  });
149
268
 
150
269
  form.addEventListener("submit", (e) => {
151
- e.preventDefault();
152
- submit.run(new FormData(form)); // double-clicks and rage-clicks are ignored
270
+ e.preventDefault();
271
+ submit.run(new FormData(form)); // double-clicks and rage-clicks are ignored
153
272
  });
154
273
  ```
155
274
 
156
- `restartable`, `exclusive`, `debounced`, `throttled`, `queue`, `concurrent`, `keyed` — each strategy is a complete, tested answer to one concurrency scenario. Swap the word, keep the rest of the code.
275
+ `restartable`, `exclusive`, `debounced`, `throttled`, `queue`, `buffered`, `concurrent`, `keyed`, `once` — each strategy is a complete, tested answer to one concurrency scenario. Swap the word, keep the rest of the code.
157
276
 
158
277
  ## What's included?
159
278
 
@@ -168,7 +287,7 @@ The library covers the states you encounter in real applications: values that ma
168
287
  - **`TaskResult<E, A>`** — a lazy async operation that can fail with a typed error.
169
288
  - **`TaskMaybe<A>`** — a lazy async operation that may produce nothing.
170
289
  - **`TaskValidation<E, A>`** — a lazy async operation that accumulates validation errors.
171
- - **`Op<I, E, A>`** — a managed async operation with a named concurrency strategy: `restartable`, `exclusive`, `debounced`, `throttled`, `queue`, `concurrent`, `keyed`, or `once`. Handles retry, timeout, cancellation, and state in one place.
290
+ - **`Op<I, E, A>`** — a managed async operation with a named concurrency strategy: `restartable`, `exclusive`, `debounced`, `throttled`, `queue`, `buffered`, `concurrent`, `keyed`, or `once`. Handles retry, timeout, cancellation, and state in one place.
172
291
  - **`RemoteData<E, A>`** — the four states of a data fetch: `NotAsked`, `Loading`, `Failure`, `Success`.
173
292
  - **`These<A, B>`** — an inclusive OR: holds a first value, a second, or both at once.
174
293
  - **`Lens<S, A>`** — focus on a required field in a nested structure. Read, set, and modify immutably.
@@ -281,10 +281,18 @@ declare namespace Result {
281
281
  */
282
282
  const recover: <E, A, B>(fallback: (e: E) => Result<E, B>) => (data: Result<E, A>) => Result<E, A | B>;
283
283
  /**
284
- * Recovers from an error unless it matches the blocked error.
284
+ * Recovers from an error unless the predicate `isBlocked` returns true for that error.
285
285
  * The fallback can produce a different success type, widening the result to `Result<E, A | B>`.
286
+ *
287
+ * @example
288
+ * ```ts
289
+ * pipe(
290
+ * Result.err(new Error("not found")),
291
+ * Result.recoverUnless(e => e.message === "fatal", () => Result.ok(0))
292
+ * ); // Ok(0)
293
+ * ```
286
294
  */
287
- const recoverUnless: <E, A, B>(blockedErr: E, fallback: () => Result<E, B>) => (data: Result<E, A>) => Result<E, A | B>;
295
+ const recoverUnless: <E, A, B>(isBlocked: (e: E) => boolean, fallback: () => Result<E, B>) => (data: Result<E, A>) => Result<E, A | B>;
288
296
  /**
289
297
  * Converts a Result to an Maybe.
290
298
  * Ok becomes Some, Err becomes None (the error is discarded).
@@ -367,11 +375,6 @@ declare namespace Maybe {
367
375
  * Extracts the value from a Maybe, returning undefined if None.
368
376
  */
369
377
  const toUndefined: <A>(data: Maybe<A>) => A | undefined;
370
- /**
371
- * Creates a Maybe from a possibly undefined value.
372
- * Returns None if undefined, Some otherwise.
373
- */
374
- const fromUndefined: <A>(value: A | undefined) => Maybe<A>;
375
378
  /**
376
379
  * Creates a Maybe from a predicate applied to a value.
377
380
  * Returns Some if the predicate passes, None otherwise.
@@ -703,10 +706,12 @@ declare namespace Task {
703
706
  const repeat: (options: {
704
707
  times: number;
705
708
  delay?: number;
706
- }) => <A>(task: Task<A>) => Task<A[]>;
709
+ }) => <A>(task: Task<A>) => Task<readonly A[]>;
707
710
  /**
708
711
  * Runs a Task repeatedly until the result satisfies a predicate, returning that result.
709
712
  * An optional delay (ms) can be inserted between runs.
713
+ * An optional `maxAttempts` cap stops the loop after N calls — the last value is returned
714
+ * regardless of whether the predicate was satisfied.
710
715
  *
711
716
  * @example
712
717
  * ```ts
@@ -719,6 +724,7 @@ declare namespace Task {
719
724
  const repeatUntil: <A>(options: {
720
725
  when: (a: A) => boolean;
721
726
  delay?: number;
727
+ maxAttempts?: number;
722
728
  }) => (task: Task<A>) => Task<A>;
723
729
  /**
724
730
  * Resolves with the value of the first Task to complete. All Tasks start
@@ -767,9 +773,12 @@ declare namespace Task {
767
773
  */
768
774
  const timeout: <E>(ms: number, onTimeout: () => E) => <A>(task: Task<A>) => Task<Result<E, A>>;
769
775
  /**
770
- * Creates a Task paired with an `abort` handle. When `abort()` is called the
771
- * `AbortSignal` passed to the factory is fired, cancelling any in-flight
772
- * operation (e.g. a `fetch`) immediately.
776
+ * Creates a Task paired with an `abort` handle. Calling `abort()` cancels the
777
+ * current in-flight call immediately. Unlike a one-shot abort, calling `task()`
778
+ * again after `abort()` starts a fresh call with a new signal.
779
+ *
780
+ * Each invocation of `task()` automatically cancels the previous in-flight call,
781
+ * making it safe to call repeatedly (e.g. on user input) without leaking promises.
773
782
  *
774
783
  * If an outer signal is also present (passed at the call site), aborting it
775
784
  * propagates into the internal controller.
@@ -281,10 +281,18 @@ declare namespace Result {
281
281
  */
282
282
  const recover: <E, A, B>(fallback: (e: E) => Result<E, B>) => (data: Result<E, A>) => Result<E, A | B>;
283
283
  /**
284
- * Recovers from an error unless it matches the blocked error.
284
+ * Recovers from an error unless the predicate `isBlocked` returns true for that error.
285
285
  * The fallback can produce a different success type, widening the result to `Result<E, A | B>`.
286
+ *
287
+ * @example
288
+ * ```ts
289
+ * pipe(
290
+ * Result.err(new Error("not found")),
291
+ * Result.recoverUnless(e => e.message === "fatal", () => Result.ok(0))
292
+ * ); // Ok(0)
293
+ * ```
286
294
  */
287
- const recoverUnless: <E, A, B>(blockedErr: E, fallback: () => Result<E, B>) => (data: Result<E, A>) => Result<E, A | B>;
295
+ const recoverUnless: <E, A, B>(isBlocked: (e: E) => boolean, fallback: () => Result<E, B>) => (data: Result<E, A>) => Result<E, A | B>;
288
296
  /**
289
297
  * Converts a Result to an Maybe.
290
298
  * Ok becomes Some, Err becomes None (the error is discarded).
@@ -367,11 +375,6 @@ declare namespace Maybe {
367
375
  * Extracts the value from a Maybe, returning undefined if None.
368
376
  */
369
377
  const toUndefined: <A>(data: Maybe<A>) => A | undefined;
370
- /**
371
- * Creates a Maybe from a possibly undefined value.
372
- * Returns None if undefined, Some otherwise.
373
- */
374
- const fromUndefined: <A>(value: A | undefined) => Maybe<A>;
375
378
  /**
376
379
  * Creates a Maybe from a predicate applied to a value.
377
380
  * Returns Some if the predicate passes, None otherwise.
@@ -703,10 +706,12 @@ declare namespace Task {
703
706
  const repeat: (options: {
704
707
  times: number;
705
708
  delay?: number;
706
- }) => <A>(task: Task<A>) => Task<A[]>;
709
+ }) => <A>(task: Task<A>) => Task<readonly A[]>;
707
710
  /**
708
711
  * Runs a Task repeatedly until the result satisfies a predicate, returning that result.
709
712
  * An optional delay (ms) can be inserted between runs.
713
+ * An optional `maxAttempts` cap stops the loop after N calls — the last value is returned
714
+ * regardless of whether the predicate was satisfied.
710
715
  *
711
716
  * @example
712
717
  * ```ts
@@ -719,6 +724,7 @@ declare namespace Task {
719
724
  const repeatUntil: <A>(options: {
720
725
  when: (a: A) => boolean;
721
726
  delay?: number;
727
+ maxAttempts?: number;
722
728
  }) => (task: Task<A>) => Task<A>;
723
729
  /**
724
730
  * Resolves with the value of the first Task to complete. All Tasks start
@@ -767,9 +773,12 @@ declare namespace Task {
767
773
  */
768
774
  const timeout: <E>(ms: number, onTimeout: () => E) => <A>(task: Task<A>) => Task<Result<E, A>>;
769
775
  /**
770
- * Creates a Task paired with an `abort` handle. When `abort()` is called the
771
- * `AbortSignal` passed to the factory is fired, cancelling any in-flight
772
- * operation (e.g. a `fetch`) immediately.
776
+ * Creates a Task paired with an `abort` handle. Calling `abort()` cancels the
777
+ * current in-flight call immediately. Unlike a one-shot abort, calling `task()`
778
+ * again after `abort()` starts a fresh call with a new signal.
779
+ *
780
+ * Each invocation of `task()` automatically cancels the previous in-flight call,
781
+ * making it safe to call repeatedly (e.g. on user input) without leaking promises.
773
782
  *
774
783
  * If an outer signal is also present (passed at the call site), aborting it
775
784
  * propagates into the internal controller.
@@ -19,7 +19,6 @@ var Maybe;
19
19
  Maybe2.fromNullable = (value) => value === null || value === void 0 ? (0, Maybe2.none)() : (0, Maybe2.some)(value);
20
20
  Maybe2.toNullable = (data) => (0, Maybe2.isSome)(data) ? data.value : null;
21
21
  Maybe2.toUndefined = (data) => (0, Maybe2.isSome)(data) ? data.value : void 0;
22
- Maybe2.fromUndefined = (value) => value === void 0 ? (0, Maybe2.none)() : (0, Maybe2.some)(value);
23
22
  Maybe2.fromPredicate = (pred) => (a) => pred(a) ? (0, Maybe2.some)(a) : (0, Maybe2.none)();
24
23
  Maybe2.toResult = (onNone) => (data) => (0, Maybe2.isSome)(data) ? Result.ok(data.value) : Result.err(onNone());
25
24
  Maybe2.fromResult = (data) => Result.isOk(data) ? (0, Maybe2.some)(data.value) : (0, Maybe2.none)();
@@ -67,7 +66,7 @@ var Result;
67
66
  };
68
67
  Result2.fromPredicate = (pred, onFalse) => (a) => pred(a) ? (0, Result2.ok)(a) : (0, Result2.err)(onFalse(a));
69
68
  Result2.recover = (fallback) => (data) => (0, Result2.isOk)(data) ? data : fallback(data.error);
70
- Result2.recoverUnless = (blockedErr, fallback) => (data) => (0, Result2.isErr)(data) && data.error !== blockedErr ? fallback() : data;
69
+ Result2.recoverUnless = (isBlocked, fallback) => (data) => (0, Result2.isErr)(data) && !isBlocked(data.error) ? fallback() : data;
71
70
  Result2.toMaybe = (data) => (0, Result2.isOk)(data) ? Maybe.some(data.value) : Maybe.none();
72
71
  Result2.ap = (arg) => (data) => (0, Result2.isOk)(data) && (0, Result2.isOk)(arg) ? (0, Result2.ok)(data.value(arg.value)) : (0, Result2.isErr)(data) ? data : arg;
73
72
  })(Result || (Result = {}));
@@ -117,13 +116,14 @@ var Task;
117
116
  return run(times);
118
117
  });
119
118
  Task2.repeatUntil = (options) => (task) => (0, Task2.from)((signal) => {
120
- const { when: predicate, delay: ms } = options;
119
+ const { when: predicate, delay: ms, maxAttempts } = options;
121
120
  const wait = () => ms !== void 0 && ms > 0 ? new Promise((r) => setTimeout(r, ms)) : Promise.resolve();
122
- const run = () => toPromise(task, signal).then((a) => {
121
+ const run = (attempt) => toPromise(task, signal).then((a) => {
123
122
  if (predicate(a)) return a;
124
- return wait().then(run);
123
+ if (maxAttempts !== void 0 && attempt >= maxAttempts) return a;
124
+ return wait().then(() => run(attempt + 1));
125
125
  });
126
- return run();
126
+ return run(1);
127
127
  });
128
128
  Task2.race = (tasks) => (0, Task2.from)((signal) => Promise.race(tasks.map((t) => toPromise(t, signal))));
129
129
  Task2.sequential = (tasks) => (0, Task2.from)(async (signal) => {
@@ -154,8 +154,12 @@ var Task;
154
154
  ]);
155
155
  });
156
156
  Task2.abortable = (factory) => {
157
- const controller = new AbortController();
157
+ let currentController = null;
158
+ const abort = () => currentController?.abort();
158
159
  const task = (outerSignal) => {
160
+ currentController?.abort();
161
+ currentController = new AbortController();
162
+ const controller = currentController;
159
163
  if (outerSignal) {
160
164
  if (outerSignal.aborted) {
161
165
  controller.abort(outerSignal.reason);
@@ -165,7 +169,7 @@ var Task;
165
169
  }
166
170
  return Deferred.fromPromise(factory(controller.signal));
167
171
  };
168
- return { task, abort: () => controller.abort() };
172
+ return { task, abort };
169
173
  };
170
174
  })(Task || (Task = {}));
171
175
 
@@ -3,7 +3,7 @@ import {
3
3
  Maybe,
4
4
  Result,
5
5
  Task
6
- } from "./chunk-HHCRWQYN.mjs";
6
+ } from "./chunk-7JF44HJH.mjs";
7
7
  import {
8
8
  isNonEmptyList
9
9
  } from "./chunk-DBIC62UV.mjs";
@@ -367,13 +367,13 @@ var Num;
367
367
  Num2.add = (b) => (a) => a + b;
368
368
  Num2.subtract = (b) => (a) => a - b;
369
369
  Num2.multiply = (b) => (a) => a * b;
370
- Num2.divide = (b) => (a) => a / b;
370
+ Num2.divide = (b) => (a) => b === 0 ? Maybe.none() : Maybe.some(a / b);
371
371
  Num2.abs = (n) => Math.abs(n);
372
372
  Num2.negate = (n) => -n;
373
373
  Num2.round = (n) => Math.round(n);
374
374
  Num2.floor = (n) => Math.floor(n);
375
375
  Num2.ceil = (n) => Math.ceil(n);
376
- Num2.remainder = (divisor) => (n) => n % divisor;
376
+ Num2.remainder = (divisor) => (n) => divisor === 0 ? Maybe.none() : Maybe.some(n % divisor);
377
377
  })(Num || (Num = {}));
378
378
 
379
379
  // src/Utils/Rec.ts
@@ -3,7 +3,7 @@ import {
3
3
  Maybe,
4
4
  Result,
5
5
  Task
6
- } from "./chunk-HHCRWQYN.mjs";
6
+ } from "./chunk-7JF44HJH.mjs";
7
7
 
8
8
  // src/Core/Lens.ts
9
9
  var Lens;
@@ -1089,6 +1089,10 @@ var RemoteData;
1089
1089
  if ((0, RemoteData2.isSuccess)(data)) f(data.value);
1090
1090
  return data;
1091
1091
  };
1092
+ RemoteData2.tapError = (f) => (data) => {
1093
+ if ((0, RemoteData2.isFailure)(data)) f(data.error);
1094
+ return data;
1095
+ };
1092
1096
  RemoteData2.recover = (fallback) => (data) => (0, RemoteData2.isFailure)(data) ? fallback(data.error) : data;
1093
1097
  RemoteData2.toMaybe = (data) => (0, RemoteData2.isSuccess)(data) ? Maybe.some(data.value) : Maybe.none();
1094
1098
  RemoteData2.toResult = (onNotReady) => (data) => (0, RemoteData2.isSuccess)(data) ? Result.ok(data.value) : Result.err((0, RemoteData2.isFailure)(data) ? data.error : onNotReady());
@@ -1227,6 +1231,7 @@ var Validation;
1227
1231
  });
1228
1232
  Validation2.isValid = (data) => data.kind === "Valid";
1229
1233
  Validation2.isInvalid = (data) => data.kind === "Invalid";
1234
+ Validation2.fromPredicate = (pred, onFalse) => (a) => pred(a) ? (0, Validation2.valid)(a) : (0, Validation2.invalid)(onFalse(a));
1230
1235
  Validation2.map = (f) => (data) => (0, Validation2.isValid)(data) ? (0, Validation2.valid)(f(data.value)) : data;
1231
1236
  Validation2.ap = (arg) => (data) => {
1232
1237
  if ((0, Validation2.isValid)(data) && (0, Validation2.isValid)(arg)) return (0, Validation2.valid)(data.value(arg.value));
@@ -1243,8 +1248,13 @@ var Validation;
1243
1248
  if ((0, Validation2.isValid)(data)) f(data.value);
1244
1249
  return data;
1245
1250
  };
1251
+ Validation2.tapError = (f) => (data) => {
1252
+ if ((0, Validation2.isInvalid)(data)) f(data.errors);
1253
+ return data;
1254
+ };
1246
1255
  Validation2.recover = (fallback) => (data) => (0, Validation2.isValid)(data) ? data : fallback(data.errors);
1247
- Validation2.recoverUnless = (blockedErrors, fallback) => (data) => (0, Validation2.isInvalid)(data) && !data.errors.some((err2) => blockedErrors.includes(err2)) ? fallback() : data;
1256
+ Validation2.recoverUnless = (isBlocked, fallback) => (data) => (0, Validation2.isInvalid)(data) && !data.errors.some(isBlocked) ? fallback() : data;
1257
+ Validation2.toResult = (data) => (0, Validation2.isValid)(data) ? Result.ok(data.value) : Result.err(data.errors);
1248
1258
  Validation2.product = (first, second) => {
1249
1259
  if ((0, Validation2.isValid)(first) && (0, Validation2.isValid)(second)) return (0, Validation2.valid)([first.value, second.value]);
1250
1260
  const errors = [