@nlozgachev/pipelined 0.26.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 +191 -72
- package/dist/{Task-7brqoQrb.d.ts → Task-BW66NsGR.d.ts} +43 -11
- package/dist/{Task-BoqaFsUR.d.mts → Task-BZT0wedE.d.mts} +43 -11
- package/dist/{chunk-Z3DYYR43.mjs → chunk-7JF44HJH.mjs} +14 -8
- package/dist/{chunk-6H373E63.mjs → chunk-CA3VE4YD.mjs} +20 -2
- package/dist/{chunk-NTISCH7Z.mjs → chunk-QJS6D6MW.mjs} +13 -2
- package/dist/core.d.mts +82 -8
- package/dist/core.d.ts +82 -8
- package/dist/core.js +26 -9
- package/dist/core.mjs +2 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +45 -10
- package/dist/index.mjs +3 -3
- package/dist/utils.d.mts +140 -5
- package/dist/utils.d.ts +140 -5
- package/dist/utils.js +33 -9
- package/dist/utils.mjs +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@nlozgachev/pipelined) [](https://github.com/nlozgachev/pipelined/actions/workflows/publish.yml) [](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
|
-
|
|
19
|
-
|
|
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:
|
|
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
|
-
|
|
32
|
-
|
|
146
|
+
| { ok: true; user: User; }
|
|
147
|
+
| { ok: false; error: "Timeout" | "NetworkError"; };
|
|
33
148
|
|
|
34
149
|
async function fetchUser(
|
|
35
|
-
|
|
36
|
-
|
|
150
|
+
id: string,
|
|
151
|
+
signal?: AbortSignal,
|
|
37
152
|
): Promise<UserResult> {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 {
|
|
71
|
-
import { Result, TaskResult } from "@nlozgachev/pipelined/core";
|
|
185
|
+
import { Op } from "@nlozgachev/pipelined/core";
|
|
72
186
|
|
|
73
|
-
const fetchUser = (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
`
|
|
85
|
-
|
|
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
|
|
90
|
-
const result = await fetchUser("42")(controller.signal);
|
|
205
|
+
const outcome = await fetchUser.run("42");
|
|
91
206
|
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
} else {
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
260
|
+
strategy: "exclusive", // in-flight? new calls are dropped immediately
|
|
142
261
|
});
|
|
143
262
|
|
|
144
263
|
submit.subscribe((state) => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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.
|
|
@@ -263,16 +263,36 @@ declare namespace Result {
|
|
|
263
263
|
* ```
|
|
264
264
|
*/
|
|
265
265
|
const tapError: <E, A>(f: (e: E) => void) => (data: Result<E, A>) => Result<E, A>;
|
|
266
|
+
/**
|
|
267
|
+
* Creates a Result from a predicate applied to a value.
|
|
268
|
+
* Returns Ok if the predicate passes, Err from onFalse otherwise.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```ts
|
|
272
|
+
* pipe(5, Result.fromPredicate(n => n > 0, n => `${n} is not positive`)); // Ok(5)
|
|
273
|
+
* pipe(-1, Result.fromPredicate(n => n > 0, n => `${n} is not positive`)); // Err("-1 is not positive")
|
|
274
|
+
* pipe("", Result.fromPredicate(s => s.length > 0, () => "empty string")); // Err("empty string")
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
const fromPredicate: <E, A>(pred: (a: A) => boolean, onFalse: (a: A) => E) => (a: A) => Result<E, A>;
|
|
266
278
|
/**
|
|
267
279
|
* Recovers from an error by providing a fallback Result.
|
|
268
280
|
* The fallback can produce a different success type, widening the result to `Result<E, A | B>`.
|
|
269
281
|
*/
|
|
270
282
|
const recover: <E, A, B>(fallback: (e: E) => Result<E, B>) => (data: Result<E, A>) => Result<E, A | B>;
|
|
271
283
|
/**
|
|
272
|
-
* Recovers from an error unless
|
|
284
|
+
* Recovers from an error unless the predicate `isBlocked` returns true for that error.
|
|
273
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
|
+
* ```
|
|
274
294
|
*/
|
|
275
|
-
const recoverUnless: <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>;
|
|
276
296
|
/**
|
|
277
297
|
* Converts a Result to an Maybe.
|
|
278
298
|
* Ok becomes Some, Err becomes None (the error is discarded).
|
|
@@ -355,11 +375,6 @@ declare namespace Maybe {
|
|
|
355
375
|
* Extracts the value from a Maybe, returning undefined if None.
|
|
356
376
|
*/
|
|
357
377
|
const toUndefined: <A>(data: Maybe<A>) => A | undefined;
|
|
358
|
-
/**
|
|
359
|
-
* Creates a Maybe from a possibly undefined value.
|
|
360
|
-
* Returns None if undefined, Some otherwise.
|
|
361
|
-
*/
|
|
362
|
-
const fromUndefined: <A>(value: A | undefined) => Maybe<A>;
|
|
363
378
|
/**
|
|
364
379
|
* Creates a Maybe from a predicate applied to a value.
|
|
365
380
|
* Returns Some if the predicate passes, None otherwise.
|
|
@@ -585,6 +600,17 @@ declare namespace Task {
|
|
|
585
600
|
* ```
|
|
586
601
|
*/
|
|
587
602
|
const from: <A>(f: (signal?: AbortSignal) => Promise<A>) => Task<A>;
|
|
603
|
+
/**
|
|
604
|
+
* Creates a Task from a lazy synchronous thunk.
|
|
605
|
+
* Unlike `Task.resolve(f())`, `fromSync` does not evaluate `f` until the Task is called.
|
|
606
|
+
*
|
|
607
|
+
* @example
|
|
608
|
+
* ```ts
|
|
609
|
+
* const t = Task.fromSync(() => Date.now()); // Date.now() not called yet
|
|
610
|
+
* const ts = await t(); // called here, every time
|
|
611
|
+
* ```
|
|
612
|
+
*/
|
|
613
|
+
const fromSync: <A>(f: () => A) => Task<A>;
|
|
588
614
|
/**
|
|
589
615
|
* Transforms the value inside a Task.
|
|
590
616
|
*
|
|
@@ -680,10 +706,12 @@ declare namespace Task {
|
|
|
680
706
|
const repeat: (options: {
|
|
681
707
|
times: number;
|
|
682
708
|
delay?: number;
|
|
683
|
-
}) => <A>(task: Task<A>) => Task<A[]>;
|
|
709
|
+
}) => <A>(task: Task<A>) => Task<readonly A[]>;
|
|
684
710
|
/**
|
|
685
711
|
* Runs a Task repeatedly until the result satisfies a predicate, returning that result.
|
|
686
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.
|
|
687
715
|
*
|
|
688
716
|
* @example
|
|
689
717
|
* ```ts
|
|
@@ -696,6 +724,7 @@ declare namespace Task {
|
|
|
696
724
|
const repeatUntil: <A>(options: {
|
|
697
725
|
when: (a: A) => boolean;
|
|
698
726
|
delay?: number;
|
|
727
|
+
maxAttempts?: number;
|
|
699
728
|
}) => (task: Task<A>) => Task<A>;
|
|
700
729
|
/**
|
|
701
730
|
* Resolves with the value of the first Task to complete. All Tasks start
|
|
@@ -744,9 +773,12 @@ declare namespace Task {
|
|
|
744
773
|
*/
|
|
745
774
|
const timeout: <E>(ms: number, onTimeout: () => E) => <A>(task: Task<A>) => Task<Result<E, A>>;
|
|
746
775
|
/**
|
|
747
|
-
* Creates a Task paired with an `abort` handle.
|
|
748
|
-
*
|
|
749
|
-
*
|
|
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.
|
|
750
782
|
*
|
|
751
783
|
* If an outer signal is also present (passed at the call site), aborting it
|
|
752
784
|
* propagates into the internal controller.
|
|
@@ -263,16 +263,36 @@ declare namespace Result {
|
|
|
263
263
|
* ```
|
|
264
264
|
*/
|
|
265
265
|
const tapError: <E, A>(f: (e: E) => void) => (data: Result<E, A>) => Result<E, A>;
|
|
266
|
+
/**
|
|
267
|
+
* Creates a Result from a predicate applied to a value.
|
|
268
|
+
* Returns Ok if the predicate passes, Err from onFalse otherwise.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```ts
|
|
272
|
+
* pipe(5, Result.fromPredicate(n => n > 0, n => `${n} is not positive`)); // Ok(5)
|
|
273
|
+
* pipe(-1, Result.fromPredicate(n => n > 0, n => `${n} is not positive`)); // Err("-1 is not positive")
|
|
274
|
+
* pipe("", Result.fromPredicate(s => s.length > 0, () => "empty string")); // Err("empty string")
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
const fromPredicate: <E, A>(pred: (a: A) => boolean, onFalse: (a: A) => E) => (a: A) => Result<E, A>;
|
|
266
278
|
/**
|
|
267
279
|
* Recovers from an error by providing a fallback Result.
|
|
268
280
|
* The fallback can produce a different success type, widening the result to `Result<E, A | B>`.
|
|
269
281
|
*/
|
|
270
282
|
const recover: <E, A, B>(fallback: (e: E) => Result<E, B>) => (data: Result<E, A>) => Result<E, A | B>;
|
|
271
283
|
/**
|
|
272
|
-
* Recovers from an error unless
|
|
284
|
+
* Recovers from an error unless the predicate `isBlocked` returns true for that error.
|
|
273
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
|
+
* ```
|
|
274
294
|
*/
|
|
275
|
-
const recoverUnless: <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>;
|
|
276
296
|
/**
|
|
277
297
|
* Converts a Result to an Maybe.
|
|
278
298
|
* Ok becomes Some, Err becomes None (the error is discarded).
|
|
@@ -355,11 +375,6 @@ declare namespace Maybe {
|
|
|
355
375
|
* Extracts the value from a Maybe, returning undefined if None.
|
|
356
376
|
*/
|
|
357
377
|
const toUndefined: <A>(data: Maybe<A>) => A | undefined;
|
|
358
|
-
/**
|
|
359
|
-
* Creates a Maybe from a possibly undefined value.
|
|
360
|
-
* Returns None if undefined, Some otherwise.
|
|
361
|
-
*/
|
|
362
|
-
const fromUndefined: <A>(value: A | undefined) => Maybe<A>;
|
|
363
378
|
/**
|
|
364
379
|
* Creates a Maybe from a predicate applied to a value.
|
|
365
380
|
* Returns Some if the predicate passes, None otherwise.
|
|
@@ -585,6 +600,17 @@ declare namespace Task {
|
|
|
585
600
|
* ```
|
|
586
601
|
*/
|
|
587
602
|
const from: <A>(f: (signal?: AbortSignal) => Promise<A>) => Task<A>;
|
|
603
|
+
/**
|
|
604
|
+
* Creates a Task from a lazy synchronous thunk.
|
|
605
|
+
* Unlike `Task.resolve(f())`, `fromSync` does not evaluate `f` until the Task is called.
|
|
606
|
+
*
|
|
607
|
+
* @example
|
|
608
|
+
* ```ts
|
|
609
|
+
* const t = Task.fromSync(() => Date.now()); // Date.now() not called yet
|
|
610
|
+
* const ts = await t(); // called here, every time
|
|
611
|
+
* ```
|
|
612
|
+
*/
|
|
613
|
+
const fromSync: <A>(f: () => A) => Task<A>;
|
|
588
614
|
/**
|
|
589
615
|
* Transforms the value inside a Task.
|
|
590
616
|
*
|
|
@@ -680,10 +706,12 @@ declare namespace Task {
|
|
|
680
706
|
const repeat: (options: {
|
|
681
707
|
times: number;
|
|
682
708
|
delay?: number;
|
|
683
|
-
}) => <A>(task: Task<A>) => Task<A[]>;
|
|
709
|
+
}) => <A>(task: Task<A>) => Task<readonly A[]>;
|
|
684
710
|
/**
|
|
685
711
|
* Runs a Task repeatedly until the result satisfies a predicate, returning that result.
|
|
686
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.
|
|
687
715
|
*
|
|
688
716
|
* @example
|
|
689
717
|
* ```ts
|
|
@@ -696,6 +724,7 @@ declare namespace Task {
|
|
|
696
724
|
const repeatUntil: <A>(options: {
|
|
697
725
|
when: (a: A) => boolean;
|
|
698
726
|
delay?: number;
|
|
727
|
+
maxAttempts?: number;
|
|
699
728
|
}) => (task: Task<A>) => Task<A>;
|
|
700
729
|
/**
|
|
701
730
|
* Resolves with the value of the first Task to complete. All Tasks start
|
|
@@ -744,9 +773,12 @@ declare namespace Task {
|
|
|
744
773
|
*/
|
|
745
774
|
const timeout: <E>(ms: number, onTimeout: () => E) => <A>(task: Task<A>) => Task<Result<E, A>>;
|
|
746
775
|
/**
|
|
747
|
-
* Creates a Task paired with an `abort` handle.
|
|
748
|
-
*
|
|
749
|
-
*
|
|
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.
|
|
750
782
|
*
|
|
751
783
|
* If an outer signal is also present (passed at the call site), aborting it
|
|
752
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)();
|
|
@@ -65,8 +64,9 @@ var Result;
|
|
|
65
64
|
if ((0, Result2.isErr)(data)) f(data.error);
|
|
66
65
|
return data;
|
|
67
66
|
};
|
|
67
|
+
Result2.fromPredicate = (pred, onFalse) => (a) => pred(a) ? (0, Result2.ok)(a) : (0, Result2.err)(onFalse(a));
|
|
68
68
|
Result2.recover = (fallback) => (data) => (0, Result2.isOk)(data) ? data : fallback(data.error);
|
|
69
|
-
Result2.recoverUnless = (
|
|
69
|
+
Result2.recoverUnless = (isBlocked, fallback) => (data) => (0, Result2.isErr)(data) && !isBlocked(data.error) ? fallback() : data;
|
|
70
70
|
Result2.toMaybe = (data) => (0, Result2.isOk)(data) ? Maybe.some(data.value) : Maybe.none();
|
|
71
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;
|
|
72
72
|
})(Result || (Result = {}));
|
|
@@ -77,6 +77,7 @@ var Task;
|
|
|
77
77
|
((Task2) => {
|
|
78
78
|
Task2.resolve = (value) => () => Deferred.fromPromise(Promise.resolve(value));
|
|
79
79
|
Task2.from = (f) => (signal) => Deferred.fromPromise(f(signal));
|
|
80
|
+
Task2.fromSync = (f) => () => Deferred.fromPromise(Promise.resolve(f()));
|
|
80
81
|
Task2.map = (f) => (data) => (0, Task2.from)((signal) => toPromise(data, signal).then(f));
|
|
81
82
|
Task2.chain = (f) => (data) => (0, Task2.from)((signal) => toPromise(data, signal).then((a) => toPromise(f(a), signal)));
|
|
82
83
|
Task2.ap = (arg) => (data) => (0, Task2.from)(
|
|
@@ -115,13 +116,14 @@ var Task;
|
|
|
115
116
|
return run(times);
|
|
116
117
|
});
|
|
117
118
|
Task2.repeatUntil = (options) => (task) => (0, Task2.from)((signal) => {
|
|
118
|
-
const { when: predicate, delay: ms } = options;
|
|
119
|
+
const { when: predicate, delay: ms, maxAttempts } = options;
|
|
119
120
|
const wait = () => ms !== void 0 && ms > 0 ? new Promise((r) => setTimeout(r, ms)) : Promise.resolve();
|
|
120
|
-
const run = () => toPromise(task, signal).then((a) => {
|
|
121
|
+
const run = (attempt) => toPromise(task, signal).then((a) => {
|
|
121
122
|
if (predicate(a)) return a;
|
|
122
|
-
|
|
123
|
+
if (maxAttempts !== void 0 && attempt >= maxAttempts) return a;
|
|
124
|
+
return wait().then(() => run(attempt + 1));
|
|
123
125
|
});
|
|
124
|
-
return run();
|
|
126
|
+
return run(1);
|
|
125
127
|
});
|
|
126
128
|
Task2.race = (tasks) => (0, Task2.from)((signal) => Promise.race(tasks.map((t) => toPromise(t, signal))));
|
|
127
129
|
Task2.sequential = (tasks) => (0, Task2.from)(async (signal) => {
|
|
@@ -152,8 +154,12 @@ var Task;
|
|
|
152
154
|
]);
|
|
153
155
|
});
|
|
154
156
|
Task2.abortable = (factory) => {
|
|
155
|
-
|
|
157
|
+
let currentController = null;
|
|
158
|
+
const abort = () => currentController?.abort();
|
|
156
159
|
const task = (outerSignal) => {
|
|
160
|
+
currentController?.abort();
|
|
161
|
+
currentController = new AbortController();
|
|
162
|
+
const controller = currentController;
|
|
157
163
|
if (outerSignal) {
|
|
158
164
|
if (outerSignal.aborted) {
|
|
159
165
|
controller.abort(outerSignal.reason);
|
|
@@ -163,7 +169,7 @@ var Task;
|
|
|
163
169
|
}
|
|
164
170
|
return Deferred.fromPromise(factory(controller.signal));
|
|
165
171
|
};
|
|
166
|
-
return { task, abort
|
|
172
|
+
return { task, abort };
|
|
167
173
|
};
|
|
168
174
|
})(Task || (Task = {}));
|
|
169
175
|
|