@nlozgachev/pipelined 0.19.0 → 0.20.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 +80 -30
- package/dist/{Task-DUdIQm-Q.d.ts → Task-BAT6Z6b9.d.ts} +32 -3
- package/dist/{Task-DBW4nOZR.d.mts → Task-JOnNAaPq.d.mts} +32 -3
- package/dist/{chunk-UWGFO7BH.mjs → chunk-7COGDULU.mjs} +54 -11
- package/dist/{chunk-B3YNH6GZ.mjs → chunk-AC7RQXWC.mjs} +1 -1
- package/dist/{chunk-EAR4TIGH.mjs → chunk-RUDOUVQR.mjs} +43 -21
- package/dist/core.d.mts +43 -9
- package/dist/core.d.ts +43 -9
- package/dist/core.js +96 -31
- package/dist/core.mjs +2 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +96 -31
- package/dist/index.mjs +3 -3
- package/dist/utils.d.mts +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +43 -21
- package/dist/utils.mjs +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,15 +10,83 @@ Opinionated functional abstractions for TypeScript.
|
|
|
10
10
|
npm add @nlozgachev/pipelined
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Possibly maybe
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
**pipelined** names every possible state and gives you operations that compose. `Maybe<A>` for values
|
|
16
|
+
that may or may not be there. `Result<E, A>` for operations that succeed or fail
|
|
17
|
+
with a typed error. `TaskResult<E, A>` for async operations that do both — lazily, with retry,
|
|
18
|
+
timeout, and cancellation built in. And, of course, there is more than that.
|
|
19
|
+
|
|
20
|
+
## Documentation
|
|
21
|
+
|
|
22
|
+
Full guides and API reference at **[pipelined.lozgachev.dev](https://pipelined.lozgachev.dev)**.
|
|
23
|
+
|
|
24
|
+
## Example
|
|
25
|
+
|
|
26
|
+
The standard approach to "fetch with retry and timeout":
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
async function fetchUser(id: string, signal?: AbortSignal): Promise<User> {
|
|
30
|
+
let lastError: unknown;
|
|
31
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(`/users/${id}`, { signal });
|
|
34
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
35
|
+
return await res.json();
|
|
36
|
+
} catch (e) {
|
|
37
|
+
if (signal?.aborted) throw e;
|
|
38
|
+
lastError = e;
|
|
39
|
+
if (attempt < 3) await new Promise(r => setTimeout(r, attempt * 1000));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
throw lastError;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The caller receives a `Promise<User>`. What it rejects with is `unknown`. The signal is checked by
|
|
47
|
+
hand. The timeout is missing. The retry loop is inlined and will need to be rewritten for the next
|
|
48
|
+
endpoint too.
|
|
49
|
+
|
|
50
|
+
With **pipelined**:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { TaskResult } from "@nlozgachev/pipelined/core";
|
|
54
|
+
import { pipe } from "@nlozgachev/pipelined/composition";
|
|
55
|
+
|
|
56
|
+
const fetchUser = (id: string): TaskResult<ApiError, User> =>
|
|
57
|
+
pipe(
|
|
58
|
+
TaskResult.tryCatch(
|
|
59
|
+
(signal) => fetch(`/users/${id}`, { signal }).then(r => r.json()),
|
|
60
|
+
(e) => new ApiError(e),
|
|
61
|
+
),
|
|
62
|
+
TaskResult.timeout(5000, () => new ApiError("request timed out")),
|
|
63
|
+
TaskResult.retry({ attempts: 3, backoff: (n) => n * 1000 }),
|
|
64
|
+
);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`TaskResult<ApiError, User>` is a lazy function — nothing runs until called. The `AbortSignal`
|
|
68
|
+
threads through every retry and the timeout automatically. The return type is the contract:
|
|
69
|
+
`ApiError` on the left, `User` on the right, nothing escapes as an exception.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
const result = await fetchUser("42")(controller.signal);
|
|
74
|
+
|
|
75
|
+
if (Result.isOk(result)) {
|
|
76
|
+
render(result.value); // User
|
|
77
|
+
} else {
|
|
78
|
+
showError(result.error.message); // ApiError, not unknown
|
|
79
|
+
}
|
|
80
|
+
```
|
|
19
81
|
|
|
20
82
|
## What's included?
|
|
21
83
|
|
|
84
|
+
`TaskResult` is one type. The library also covers the rest of the states you encounter in real
|
|
85
|
+
applications: values that may be absent, operations that accumulate multiple errors, data that moves
|
|
86
|
+
through `NotAsked >> Loading >> ( Success | Failure )`, nested immutable updates, and computations that
|
|
87
|
+
share a common environment. Every type follows the same conventions — `map`, `chain`, `match`,
|
|
88
|
+
`getOrElse` — so moving between them feels familiar.
|
|
89
|
+
|
|
22
90
|
### pipelined/core
|
|
23
91
|
|
|
24
92
|
- **`Maybe<A>`** — a value that may not exist; propagates absence without null checks.
|
|
@@ -45,9 +113,16 @@ Everyday utilities for built-in JS types.
|
|
|
45
113
|
|
|
46
114
|
- **`Arr`** — array utilities, data-last, returning `Maybe` instead of `undefined`.
|
|
47
115
|
- **`Rec`** — record/object utilities, data-last, with `Maybe`-returning key lookup.
|
|
116
|
+
- **`Dict`** — `ReadonlyMap<K, V>` utilities: `lookup`, `groupBy`, `upsert`, set operations.
|
|
117
|
+
- **`Uniq`** — `ReadonlySet<A>` utilities: `insert`, `remove`, `union`, `intersection`, `difference`.
|
|
48
118
|
- **`Num`** — number utilities: `range`, `clamp`, `between`, safe `parse`, and curried arithmetic.
|
|
49
119
|
- **`Str`** — string utilities: `split`, `trim`, `words`, `lines`, and safe `parse.int` / `parse.float`.
|
|
50
120
|
|
|
121
|
+
Every utility is benchmarked against its native equivalent. The data-last currying adds a function
|
|
122
|
+
call; that is the expected cost of composability. Operations that exceeded a reasonable overhead
|
|
123
|
+
have custom implementations that in several cases run faster than the native method they replace. See the
|
|
124
|
+
[benchmarks page](https://pipelined.lozgachev.dev/appendix/benchmarks) for the methodology.
|
|
125
|
+
|
|
51
126
|
### pipelined/types
|
|
52
127
|
|
|
53
128
|
- **`Brand<K, T>`** — nominal typing at compile time, zero runtime cost.
|
|
@@ -58,33 +133,8 @@ Everyday utilities for built-in JS types.
|
|
|
58
133
|
- **`pipe`**, **`flow`**, **`compose`** — function composition.
|
|
59
134
|
- **`curry`** / **`uncurry`**, **`tap`**, **`memoize`**, and other function utilities.
|
|
60
135
|
|
|
61
|
-
## Example
|
|
62
136
|
|
|
63
|
-
```ts
|
|
64
|
-
import { Maybe, Result } from "@nlozgachev/pipelined/core";
|
|
65
|
-
import { pipe } from "@nlozgachev/pipelined/composition";
|
|
66
|
-
|
|
67
|
-
// Chain nullable lookups without nested null checks
|
|
68
|
-
const city = pipe(
|
|
69
|
-
getUser(userId), // User | null
|
|
70
|
-
Maybe.fromNullable, // Maybe<User>
|
|
71
|
-
Maybe.chain((u) => Maybe.fromNullable(u.address)), // Maybe<Address>
|
|
72
|
-
Maybe.chain((a) => Maybe.fromNullable(a.city)), // Maybe<string>
|
|
73
|
-
Maybe.map((c) => c.toUpperCase()), // Maybe<string>
|
|
74
|
-
Maybe.getOrElse("UNKNOWN"), // string
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
// Parse input and look up a record — both steps can fail
|
|
78
|
-
const record = pipe(
|
|
79
|
-
parseId(rawInput), // Result<ParseError, number>
|
|
80
|
-
Result.chain((id) => db.find(id)), // Result<ParseError | NotFoundError, Record>
|
|
81
|
-
Result.map((r) => r.name), // Result<ParseError | NotFoundError, string>
|
|
82
|
-
);
|
|
83
|
-
```
|
|
84
137
|
|
|
85
|
-
## Documentation
|
|
86
|
-
|
|
87
|
-
Full guides and API reference at **[pipelined.lozgachev.dev](https://pipelined.lozgachev.dev)**.
|
|
88
138
|
|
|
89
139
|
## License
|
|
90
140
|
|
|
@@ -465,6 +465,10 @@ declare namespace Maybe {
|
|
|
465
465
|
* - **Infallible** — it never rejects. If failure is possible, encode it in the
|
|
466
466
|
* return type using `TaskResult<E, A>` instead.
|
|
467
467
|
*
|
|
468
|
+
* An optional `AbortSignal` can be passed at the call site. Combinators like
|
|
469
|
+
* `retry`, `pollUntil`, and `timeout` thread it automatically to every inner
|
|
470
|
+
* operation. Existing tasks that ignore the signal continue to work unchanged.
|
|
471
|
+
*
|
|
468
472
|
* Calling a Task returns a `Deferred<A>` — a one-shot async value that supports
|
|
469
473
|
* `await` but has no `.catch()`, `.finally()`, or chainable `.then()`.
|
|
470
474
|
*
|
|
@@ -495,7 +499,7 @@ declare namespace Maybe {
|
|
|
495
499
|
* const result = await formatted();
|
|
496
500
|
* ```
|
|
497
501
|
*/
|
|
498
|
-
type Task<A> = () => Deferred<A>;
|
|
502
|
+
type Task<A> = (signal?: AbortSignal) => Deferred<A>;
|
|
499
503
|
declare namespace Task {
|
|
500
504
|
/**
|
|
501
505
|
* Creates a Task that immediately resolves to the given value.
|
|
@@ -509,13 +513,14 @@ declare namespace Task {
|
|
|
509
513
|
const resolve: <A>(value: A) => Task<A>;
|
|
510
514
|
/**
|
|
511
515
|
* Creates a Task from a function that returns a Promise.
|
|
516
|
+
* The factory optionally receives an `AbortSignal` forwarded from the call site.
|
|
512
517
|
*
|
|
513
518
|
* @example
|
|
514
519
|
* ```ts
|
|
515
520
|
* const getTimestamp = Task.from(() => Promise.resolve(Date.now()));
|
|
516
521
|
* ```
|
|
517
522
|
*/
|
|
518
|
-
const from: <A>(f: () => Promise<A>) => Task<A>;
|
|
523
|
+
const from: <A>(f: (signal?: AbortSignal) => Promise<A>) => Task<A>;
|
|
519
524
|
/**
|
|
520
525
|
* Transforms the value inside a Task.
|
|
521
526
|
*
|
|
@@ -660,7 +665,9 @@ declare namespace Task {
|
|
|
660
665
|
const sequential: <A>(tasks: ReadonlyArray<Task<A>>) => Task<ReadonlyArray<A>>;
|
|
661
666
|
/**
|
|
662
667
|
* Converts a `Task<A>` into a `Task<Result<E, A>>`, resolving to `Err` if the
|
|
663
|
-
* Task does not complete within the given time.
|
|
668
|
+
* Task does not complete within the given time. The inner Task receives an
|
|
669
|
+
* `AbortSignal` that fires when the deadline passes, so operations like `fetch`
|
|
670
|
+
* that accept a signal are cancelled rather than left dangling.
|
|
664
671
|
*
|
|
665
672
|
* @example
|
|
666
673
|
* ```ts
|
|
@@ -672,6 +679,28 @@ declare namespace Task {
|
|
|
672
679
|
* ```
|
|
673
680
|
*/
|
|
674
681
|
const timeout: <E>(ms: number, onTimeout: () => E) => <A>(task: Task<A>) => Task<Result<E, A>>;
|
|
682
|
+
/**
|
|
683
|
+
* Creates a Task paired with an `abort` handle. When `abort()` is called the
|
|
684
|
+
* `AbortSignal` passed to the factory is fired, cancelling any in-flight
|
|
685
|
+
* operation (e.g. a `fetch`) immediately.
|
|
686
|
+
*
|
|
687
|
+
* If an outer signal is also present (passed at the call site), aborting it
|
|
688
|
+
* propagates into the internal controller.
|
|
689
|
+
*
|
|
690
|
+
* @example
|
|
691
|
+
* ```ts
|
|
692
|
+
* const { task: poll, abort } = Task.abortable(
|
|
693
|
+
* (signal) => waitForEvent(bus, "ready", { signal }),
|
|
694
|
+
* );
|
|
695
|
+
*
|
|
696
|
+
* onUnmount(abort);
|
|
697
|
+
* await poll();
|
|
698
|
+
* ```
|
|
699
|
+
*/
|
|
700
|
+
const abortable: <A>(factory: (signal: AbortSignal) => Promise<A>) => {
|
|
701
|
+
task: Task<A>;
|
|
702
|
+
abort: () => void;
|
|
703
|
+
};
|
|
675
704
|
}
|
|
676
705
|
|
|
677
706
|
export { Deferred as D, type Err as E, Maybe as M, type None as N, type Ok as O, Result as R, type Some as S, Task as T, type WithValue as W, type WithLog as a, type WithKind as b, type WithError as c, type WithErrors as d, type WithFirst as e, type WithSecond as f };
|
|
@@ -465,6 +465,10 @@ declare namespace Maybe {
|
|
|
465
465
|
* - **Infallible** — it never rejects. If failure is possible, encode it in the
|
|
466
466
|
* return type using `TaskResult<E, A>` instead.
|
|
467
467
|
*
|
|
468
|
+
* An optional `AbortSignal` can be passed at the call site. Combinators like
|
|
469
|
+
* `retry`, `pollUntil`, and `timeout` thread it automatically to every inner
|
|
470
|
+
* operation. Existing tasks that ignore the signal continue to work unchanged.
|
|
471
|
+
*
|
|
468
472
|
* Calling a Task returns a `Deferred<A>` — a one-shot async value that supports
|
|
469
473
|
* `await` but has no `.catch()`, `.finally()`, or chainable `.then()`.
|
|
470
474
|
*
|
|
@@ -495,7 +499,7 @@ declare namespace Maybe {
|
|
|
495
499
|
* const result = await formatted();
|
|
496
500
|
* ```
|
|
497
501
|
*/
|
|
498
|
-
type Task<A> = () => Deferred<A>;
|
|
502
|
+
type Task<A> = (signal?: AbortSignal) => Deferred<A>;
|
|
499
503
|
declare namespace Task {
|
|
500
504
|
/**
|
|
501
505
|
* Creates a Task that immediately resolves to the given value.
|
|
@@ -509,13 +513,14 @@ declare namespace Task {
|
|
|
509
513
|
const resolve: <A>(value: A) => Task<A>;
|
|
510
514
|
/**
|
|
511
515
|
* Creates a Task from a function that returns a Promise.
|
|
516
|
+
* The factory optionally receives an `AbortSignal` forwarded from the call site.
|
|
512
517
|
*
|
|
513
518
|
* @example
|
|
514
519
|
* ```ts
|
|
515
520
|
* const getTimestamp = Task.from(() => Promise.resolve(Date.now()));
|
|
516
521
|
* ```
|
|
517
522
|
*/
|
|
518
|
-
const from: <A>(f: () => Promise<A>) => Task<A>;
|
|
523
|
+
const from: <A>(f: (signal?: AbortSignal) => Promise<A>) => Task<A>;
|
|
519
524
|
/**
|
|
520
525
|
* Transforms the value inside a Task.
|
|
521
526
|
*
|
|
@@ -660,7 +665,9 @@ declare namespace Task {
|
|
|
660
665
|
const sequential: <A>(tasks: ReadonlyArray<Task<A>>) => Task<ReadonlyArray<A>>;
|
|
661
666
|
/**
|
|
662
667
|
* Converts a `Task<A>` into a `Task<Result<E, A>>`, resolving to `Err` if the
|
|
663
|
-
* Task does not complete within the given time.
|
|
668
|
+
* Task does not complete within the given time. The inner Task receives an
|
|
669
|
+
* `AbortSignal` that fires when the deadline passes, so operations like `fetch`
|
|
670
|
+
* that accept a signal are cancelled rather than left dangling.
|
|
664
671
|
*
|
|
665
672
|
* @example
|
|
666
673
|
* ```ts
|
|
@@ -672,6 +679,28 @@ declare namespace Task {
|
|
|
672
679
|
* ```
|
|
673
680
|
*/
|
|
674
681
|
const timeout: <E>(ms: number, onTimeout: () => E) => <A>(task: Task<A>) => Task<Result<E, A>>;
|
|
682
|
+
/**
|
|
683
|
+
* Creates a Task paired with an `abort` handle. When `abort()` is called the
|
|
684
|
+
* `AbortSignal` passed to the factory is fired, cancelling any in-flight
|
|
685
|
+
* operation (e.g. a `fetch`) immediately.
|
|
686
|
+
*
|
|
687
|
+
* If an outer signal is also present (passed at the call site), aborting it
|
|
688
|
+
* propagates into the internal controller.
|
|
689
|
+
*
|
|
690
|
+
* @example
|
|
691
|
+
* ```ts
|
|
692
|
+
* const { task: poll, abort } = Task.abortable(
|
|
693
|
+
* (signal) => waitForEvent(bus, "ready", { signal }),
|
|
694
|
+
* );
|
|
695
|
+
*
|
|
696
|
+
* onUnmount(abort);
|
|
697
|
+
* await poll();
|
|
698
|
+
* ```
|
|
699
|
+
*/
|
|
700
|
+
const abortable: <A>(factory: (signal: AbortSignal) => Promise<A>) => {
|
|
701
|
+
task: Task<A>;
|
|
702
|
+
abort: () => void;
|
|
703
|
+
};
|
|
675
704
|
}
|
|
676
705
|
|
|
677
706
|
export { Deferred as D, type Err as E, Maybe as M, type None as N, type Ok as O, Result as R, type Some as S, Task as T, type WithValue as W, type WithLog as a, type WithKind as b, type WithError as c, type WithErrors as d, type WithFirst as e, type WithSecond as f };
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
Maybe,
|
|
4
4
|
Result,
|
|
5
5
|
Task
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-RUDOUVQR.mjs";
|
|
7
7
|
|
|
8
8
|
// src/Core/Lens.ts
|
|
9
9
|
var Lens;
|
|
@@ -312,12 +312,23 @@ var TaskMaybe;
|
|
|
312
312
|
})(TaskMaybe || (TaskMaybe = {}));
|
|
313
313
|
|
|
314
314
|
// src/Core/TaskResult.ts
|
|
315
|
+
var cancellableWait = (ms, signal) => {
|
|
316
|
+
if (ms <= 0) return Promise.resolve();
|
|
317
|
+
if (!signal) return new Promise((r) => setTimeout(r, ms));
|
|
318
|
+
return new Promise((resolve) => {
|
|
319
|
+
const id = setTimeout(resolve, ms);
|
|
320
|
+
signal.addEventListener("abort", () => {
|
|
321
|
+
clearTimeout(id);
|
|
322
|
+
resolve();
|
|
323
|
+
}, { once: true });
|
|
324
|
+
});
|
|
325
|
+
};
|
|
315
326
|
var TaskResult;
|
|
316
327
|
((TaskResult2) => {
|
|
317
328
|
TaskResult2.ok = (value) => Task.resolve(Result.ok(value));
|
|
318
329
|
TaskResult2.err = (error) => Task.resolve(Result.err(error));
|
|
319
330
|
TaskResult2.tryCatch = (f, onError) => Task.from(
|
|
320
|
-
() => f().then(Result.ok).catch((e) => Result.err(onError(e)))
|
|
331
|
+
(signal) => f(signal).then(Result.ok).catch((e) => Result.err(onError(e)))
|
|
321
332
|
);
|
|
322
333
|
TaskResult2.map = (f) => (data) => Task.map(Result.map(f))(data);
|
|
323
334
|
TaskResult2.mapError = (f) => (data) => Task.map(Result.mapError(f))(data);
|
|
@@ -331,43 +342,75 @@ var TaskResult;
|
|
|
331
342
|
)(data);
|
|
332
343
|
TaskResult2.getOrElse = (defaultValue) => (data) => Task.map(Result.getOrElse(defaultValue))(data);
|
|
333
344
|
TaskResult2.tap = (f) => (data) => Task.map(Result.tap(f))(data);
|
|
334
|
-
TaskResult2.retry = (options) => (data) => Task.from(() => {
|
|
345
|
+
TaskResult2.retry = (options) => (data) => Task.from((signal) => {
|
|
335
346
|
const { attempts, backoff, when: shouldRetry } = options;
|
|
336
347
|
const getDelay = (n) => backoff === void 0 ? 0 : typeof backoff === "function" ? backoff(n) : backoff;
|
|
337
|
-
const run = (left) => Deferred.toPromise(data()).then((result) => {
|
|
348
|
+
const run = (left) => Deferred.toPromise(data(signal)).then((result) => {
|
|
338
349
|
if (Result.isOk(result)) return result;
|
|
339
350
|
if (left <= 1) return result;
|
|
340
351
|
if (shouldRetry !== void 0 && !shouldRetry(result.error)) {
|
|
341
352
|
return result;
|
|
342
353
|
}
|
|
354
|
+
if (signal?.aborted) return result;
|
|
343
355
|
const ms = getDelay(attempts - left + 1);
|
|
344
|
-
return (ms
|
|
356
|
+
return cancellableWait(ms, signal).then(() => {
|
|
357
|
+
if (signal?.aborted) return result;
|
|
358
|
+
return run(left - 1);
|
|
359
|
+
});
|
|
345
360
|
});
|
|
346
361
|
return run(attempts);
|
|
347
362
|
});
|
|
348
|
-
TaskResult2.pollUntil = (options) => (task) => Task.from(() => {
|
|
363
|
+
TaskResult2.pollUntil = (options) => (task) => Task.from((signal) => {
|
|
349
364
|
const { when: predicate, delay } = options;
|
|
350
365
|
const getDelay = (attempt) => delay === void 0 ? 0 : typeof delay === "function" ? delay(attempt) : delay;
|
|
351
|
-
const run = (attempt) => Deferred.toPromise(task()).then((result) => {
|
|
366
|
+
const run = (attempt) => Deferred.toPromise(task(signal)).then((result) => {
|
|
352
367
|
if (Result.isErr(result)) return result;
|
|
353
368
|
if (predicate(result.value)) return result;
|
|
369
|
+
if (signal?.aborted) return result;
|
|
354
370
|
const ms = getDelay(attempt);
|
|
355
|
-
return (ms
|
|
371
|
+
return cancellableWait(ms, signal).then(() => {
|
|
372
|
+
if (signal?.aborted) return result;
|
|
373
|
+
return run(attempt + 1);
|
|
374
|
+
});
|
|
356
375
|
});
|
|
357
376
|
return run(1);
|
|
358
377
|
});
|
|
359
|
-
TaskResult2.timeout = (ms, onTimeout) => (data) => Task.from(() => {
|
|
378
|
+
TaskResult2.timeout = (ms, onTimeout) => (data) => Task.from((outerSignal) => {
|
|
379
|
+
const controller = new AbortController();
|
|
380
|
+
const onOuterAbort = () => controller.abort();
|
|
381
|
+
outerSignal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
360
382
|
let timerId;
|
|
361
383
|
return Promise.race([
|
|
362
|
-
Deferred.toPromise(data()).then((result) => {
|
|
384
|
+
Deferred.toPromise(data(controller.signal)).then((result) => {
|
|
363
385
|
clearTimeout(timerId);
|
|
386
|
+
outerSignal?.removeEventListener("abort", onOuterAbort);
|
|
364
387
|
return result;
|
|
365
388
|
}),
|
|
366
389
|
new Promise((resolve) => {
|
|
367
|
-
timerId = setTimeout(() =>
|
|
390
|
+
timerId = setTimeout(() => {
|
|
391
|
+
controller.abort();
|
|
392
|
+
outerSignal?.removeEventListener("abort", onOuterAbort);
|
|
393
|
+
resolve(Result.err(onTimeout()));
|
|
394
|
+
}, ms);
|
|
368
395
|
})
|
|
369
396
|
]);
|
|
370
397
|
});
|
|
398
|
+
TaskResult2.abortable = (factory, onError) => {
|
|
399
|
+
const controller = new AbortController();
|
|
400
|
+
const task = (outerSignal) => {
|
|
401
|
+
if (outerSignal) {
|
|
402
|
+
if (outerSignal.aborted) {
|
|
403
|
+
controller.abort(outerSignal.reason);
|
|
404
|
+
} else {
|
|
405
|
+
outerSignal.addEventListener("abort", () => controller.abort(outerSignal.reason), { once: true });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return Deferred.fromPromise(
|
|
409
|
+
factory(controller.signal).then(Result.ok).catch((e) => Result.err(onError(e)))
|
|
410
|
+
);
|
|
411
|
+
};
|
|
412
|
+
return { task, abort: () => controller.abort() };
|
|
413
|
+
};
|
|
371
414
|
})(TaskResult || (TaskResult = {}));
|
|
372
415
|
|
|
373
416
|
// src/Core/Validation.ts
|
|
@@ -69,77 +69,99 @@ var Result;
|
|
|
69
69
|
})(Result || (Result = {}));
|
|
70
70
|
|
|
71
71
|
// src/Core/Task.ts
|
|
72
|
-
var toPromise = (task) => Deferred.toPromise(task());
|
|
72
|
+
var toPromise = (task, signal) => Deferred.toPromise(task(signal));
|
|
73
73
|
var Task;
|
|
74
74
|
((Task2) => {
|
|
75
75
|
Task2.resolve = (value) => () => Deferred.fromPromise(Promise.resolve(value));
|
|
76
|
-
Task2.from = (f) => () => Deferred.fromPromise(f());
|
|
77
|
-
Task2.map = (f) => (data) => (0, Task2.from)(() => toPromise(data).then(f));
|
|
78
|
-
Task2.chain = (f) => (data) => (0, Task2.from)(() => toPromise(data).then((a) => toPromise(f(a))));
|
|
76
|
+
Task2.from = (f) => (signal) => Deferred.fromPromise(f(signal));
|
|
77
|
+
Task2.map = (f) => (data) => (0, Task2.from)((signal) => toPromise(data, signal).then(f));
|
|
78
|
+
Task2.chain = (f) => (data) => (0, Task2.from)((signal) => toPromise(data, signal).then((a) => toPromise(f(a), signal)));
|
|
79
79
|
Task2.ap = (arg) => (data) => (0, Task2.from)(
|
|
80
|
-
() => Promise.all([
|
|
81
|
-
toPromise(data),
|
|
82
|
-
toPromise(arg)
|
|
80
|
+
(signal) => Promise.all([
|
|
81
|
+
toPromise(data, signal),
|
|
82
|
+
toPromise(arg, signal)
|
|
83
83
|
]).then(([f, a]) => f(a))
|
|
84
84
|
);
|
|
85
85
|
Task2.tap = (f) => (data) => (0, Task2.from)(
|
|
86
|
-
() => toPromise(data).then((a) => {
|
|
86
|
+
(signal) => toPromise(data, signal).then((a) => {
|
|
87
87
|
f(a);
|
|
88
88
|
return a;
|
|
89
89
|
})
|
|
90
90
|
);
|
|
91
91
|
Task2.all = (tasks) => (0, Task2.from)(
|
|
92
|
-
() => Promise.all(tasks.map((t) => toPromise(t)))
|
|
92
|
+
(signal) => Promise.all(tasks.map((t) => toPromise(t, signal)))
|
|
93
93
|
);
|
|
94
94
|
Task2.delay = (ms) => (data) => (0, Task2.from)(
|
|
95
|
-
() => new Promise(
|
|
95
|
+
(signal) => new Promise(
|
|
96
96
|
(resolve2) => setTimeout(
|
|
97
|
-
() => toPromise(data).then(resolve2),
|
|
97
|
+
() => toPromise(data, signal).then(resolve2),
|
|
98
98
|
ms
|
|
99
99
|
)
|
|
100
100
|
)
|
|
101
101
|
);
|
|
102
|
-
Task2.repeat = (options) => (task) => (0, Task2.from)(() => {
|
|
102
|
+
Task2.repeat = (options) => (task) => (0, Task2.from)((signal) => {
|
|
103
103
|
const { times, delay: ms } = options;
|
|
104
104
|
if (times <= 0) return Promise.resolve([]);
|
|
105
105
|
const results = [];
|
|
106
106
|
const wait = () => ms !== void 0 && ms > 0 ? new Promise((r) => setTimeout(r, ms)) : Promise.resolve();
|
|
107
|
-
const run = (left) => toPromise(task).then((a) => {
|
|
107
|
+
const run = (left) => toPromise(task, signal).then((a) => {
|
|
108
108
|
results.push(a);
|
|
109
109
|
if (left <= 1) return results;
|
|
110
110
|
return wait().then(() => run(left - 1));
|
|
111
111
|
});
|
|
112
112
|
return run(times);
|
|
113
113
|
});
|
|
114
|
-
Task2.repeatUntil = (options) => (task) => (0, Task2.from)(() => {
|
|
114
|
+
Task2.repeatUntil = (options) => (task) => (0, Task2.from)((signal) => {
|
|
115
115
|
const { when: predicate, delay: ms } = options;
|
|
116
116
|
const wait = () => ms !== void 0 && ms > 0 ? new Promise((r) => setTimeout(r, ms)) : Promise.resolve();
|
|
117
|
-
const run = () => toPromise(task).then((a) => {
|
|
117
|
+
const run = () => toPromise(task, signal).then((a) => {
|
|
118
118
|
if (predicate(a)) return a;
|
|
119
119
|
return wait().then(run);
|
|
120
120
|
});
|
|
121
121
|
return run();
|
|
122
122
|
});
|
|
123
|
-
Task2.race = (tasks) => (0, Task2.from)(() => Promise.race(tasks.map(toPromise)));
|
|
124
|
-
Task2.sequential = (tasks) => (0, Task2.from)(async () => {
|
|
123
|
+
Task2.race = (tasks) => (0, Task2.from)((signal) => Promise.race(tasks.map((t) => toPromise(t, signal))));
|
|
124
|
+
Task2.sequential = (tasks) => (0, Task2.from)(async (signal) => {
|
|
125
125
|
const results = [];
|
|
126
126
|
for (const task of tasks) {
|
|
127
|
-
results.push(await toPromise(task));
|
|
127
|
+
results.push(await toPromise(task, signal));
|
|
128
128
|
}
|
|
129
129
|
return results;
|
|
130
130
|
});
|
|
131
|
-
Task2.timeout = (ms, onTimeout) => (task) => (0, Task2.from)(() => {
|
|
131
|
+
Task2.timeout = (ms, onTimeout) => (task) => (0, Task2.from)((outerSignal) => {
|
|
132
|
+
const controller = new AbortController();
|
|
133
|
+
const onOuterAbort = () => controller.abort();
|
|
134
|
+
outerSignal?.addEventListener("abort", onOuterAbort, { once: true });
|
|
132
135
|
let timerId;
|
|
133
136
|
return Promise.race([
|
|
134
|
-
toPromise(task).then((a) => {
|
|
137
|
+
toPromise(task, controller.signal).then((a) => {
|
|
135
138
|
clearTimeout(timerId);
|
|
139
|
+
outerSignal?.removeEventListener("abort", onOuterAbort);
|
|
136
140
|
return Result.ok(a);
|
|
137
141
|
}),
|
|
138
142
|
new Promise((resolve2) => {
|
|
139
|
-
timerId = setTimeout(() =>
|
|
143
|
+
timerId = setTimeout(() => {
|
|
144
|
+
controller.abort();
|
|
145
|
+
outerSignal?.removeEventListener("abort", onOuterAbort);
|
|
146
|
+
resolve2(Result.err(onTimeout()));
|
|
147
|
+
}, ms);
|
|
140
148
|
})
|
|
141
149
|
]);
|
|
142
150
|
});
|
|
151
|
+
Task2.abortable = (factory) => {
|
|
152
|
+
const controller = new AbortController();
|
|
153
|
+
const task = (outerSignal) => {
|
|
154
|
+
if (outerSignal) {
|
|
155
|
+
if (outerSignal.aborted) {
|
|
156
|
+
controller.abort(outerSignal.reason);
|
|
157
|
+
} else {
|
|
158
|
+
outerSignal.addEventListener("abort", () => controller.abort(outerSignal.reason), { once: true });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return Deferred.fromPromise(factory(controller.signal));
|
|
162
|
+
};
|
|
163
|
+
return { task, abort: () => controller.abort() };
|
|
164
|
+
};
|
|
143
165
|
})(Task || (Task = {}));
|
|
144
166
|
|
|
145
167
|
export {
|
package/dist/core.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { M as Maybe, W as WithValue, a as WithLog, R as Result, b as WithKind, c as WithError, T as Task, d as WithErrors, e as WithFirst, f as WithSecond } from './Task-
|
|
2
|
-
export { D as Deferred, E as Err, N as None, O as Ok, S as Some } from './Task-
|
|
1
|
+
import { M as Maybe, W as WithValue, a as WithLog, R as Result, b as WithKind, c as WithError, T as Task, d as WithErrors, e as WithFirst, f as WithSecond } from './Task-JOnNAaPq.mjs';
|
|
2
|
+
export { D as Deferred, E as Err, N as None, O as Ok, S as Some } from './Task-JOnNAaPq.mjs';
|
|
3
3
|
import { N as NonEmptyList } from './NonEmptyList-BlGFjor5.mjs';
|
|
4
4
|
|
|
5
5
|
/** Keys of T for which undefined is assignable (i.e. optional fields). */
|
|
@@ -1053,7 +1053,7 @@ declare namespace RemoteData {
|
|
|
1053
1053
|
* ```ts
|
|
1054
1054
|
* const fetchUser = (id: string): TaskResult<Error, User> =>
|
|
1055
1055
|
* TaskResult.tryCatch(
|
|
1056
|
-
* () => fetch(`/users/${id}
|
|
1056
|
+
* (signal) => fetch(`/users/${id}`, { signal }).then(r => r.json()),
|
|
1057
1057
|
* (e) => new Error(`Failed to fetch user: ${e}`)
|
|
1058
1058
|
* );
|
|
1059
1059
|
* ```
|
|
@@ -1071,17 +1071,18 @@ declare namespace TaskResult {
|
|
|
1071
1071
|
/**
|
|
1072
1072
|
* Creates a TaskResult from a function that may throw.
|
|
1073
1073
|
* Catches any errors and transforms them using the onError function.
|
|
1074
|
+
* The factory optionally receives an `AbortSignal` forwarded from the call site.
|
|
1074
1075
|
*
|
|
1075
1076
|
* @example
|
|
1076
1077
|
* ```ts
|
|
1077
|
-
* const
|
|
1078
|
+
* const fetchUser = (id: string): TaskResult<string, User> =>
|
|
1078
1079
|
* TaskResult.tryCatch(
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1080
|
+
* (signal) => fetch(`/users/${id}`, { signal }).then(r => r.json()),
|
|
1081
|
+
* String
|
|
1081
1082
|
* );
|
|
1082
1083
|
* ```
|
|
1083
1084
|
*/
|
|
1084
|
-
const tryCatch: <E, A>(f: () => Promise<A>, onError: (e: unknown) => E) => TaskResult<E, A>;
|
|
1085
|
+
const tryCatch: <E, A>(f: (signal?: AbortSignal) => Promise<A>, onError: (e: unknown) => E) => TaskResult<E, A>;
|
|
1085
1086
|
/**
|
|
1086
1087
|
* Transforms the success value inside a TaskResult.
|
|
1087
1088
|
*/
|
|
@@ -1123,6 +1124,8 @@ declare namespace TaskResult {
|
|
|
1123
1124
|
const tap: <E, A>(f: (a: A) => void) => (data: TaskResult<E, A>) => TaskResult<E, A>;
|
|
1124
1125
|
/**
|
|
1125
1126
|
* Re-runs a TaskResult on `Err` with configurable attempts, backoff, and retry condition.
|
|
1127
|
+
* An `AbortSignal` passed at the call site is forwarded to each attempt; the loop also
|
|
1128
|
+
* checks the signal before starting a new attempt so cancellation stops the loop promptly.
|
|
1126
1129
|
*
|
|
1127
1130
|
* @param options.attempts - Total number of attempts (1 = no retry, 3 = up to 3 tries)
|
|
1128
1131
|
* @param options.backoff - Fixed delay in ms, or a function `(attempt) => ms` for computed delay
|
|
@@ -1151,6 +1154,8 @@ declare namespace TaskResult {
|
|
|
1151
1154
|
/**
|
|
1152
1155
|
* Polls a TaskResult repeatedly until the success value satisfies a predicate.
|
|
1153
1156
|
* Stops immediately and returns `Err` if the task fails.
|
|
1157
|
+
* An `AbortSignal` passed at the call site is forwarded to each attempt; the loop
|
|
1158
|
+
* also checks the signal before starting a new poll so cancellation stops promptly.
|
|
1154
1159
|
*
|
|
1155
1160
|
* `delay` accepts a fixed number of milliseconds or a function `(attempt) => ms`
|
|
1156
1161
|
* for a computed delay — useful for starting fast and slowing down over time.
|
|
@@ -1158,7 +1163,10 @@ declare namespace TaskResult {
|
|
|
1158
1163
|
* @example
|
|
1159
1164
|
* ```ts
|
|
1160
1165
|
* const checkJob = (id: string): TaskResult<string, { status: "pending" | "done" }> =>
|
|
1161
|
-
* TaskResult.tryCatch(
|
|
1166
|
+
* TaskResult.tryCatch(
|
|
1167
|
+
* (signal) => fetch(`/jobs/${id}`, { signal }).then(r => r.json()),
|
|
1168
|
+
* String
|
|
1169
|
+
* );
|
|
1162
1170
|
*
|
|
1163
1171
|
* // Fixed delay: poll every 2s
|
|
1164
1172
|
* pipe(
|
|
@@ -1179,7 +1187,8 @@ declare namespace TaskResult {
|
|
|
1179
1187
|
}) => <E>(task: TaskResult<E, A>) => TaskResult<E, A>;
|
|
1180
1188
|
/**
|
|
1181
1189
|
* Fails a TaskResult with a typed error if it does not resolve within the given time.
|
|
1182
|
-
*
|
|
1190
|
+
* The inner task receives an `AbortSignal` that fires when the deadline passes —
|
|
1191
|
+
* operations like `fetch` that accept a signal are cancelled rather than left dangling.
|
|
1183
1192
|
*
|
|
1184
1193
|
* @example
|
|
1185
1194
|
* ```ts
|
|
@@ -1190,6 +1199,31 @@ declare namespace TaskResult {
|
|
|
1190
1199
|
* ```
|
|
1191
1200
|
*/
|
|
1192
1201
|
const timeout: <E>(ms: number, onTimeout: () => E) => <A>(data: TaskResult<E, A>) => TaskResult<E, A>;
|
|
1202
|
+
/**
|
|
1203
|
+
* Creates a TaskResult paired with an `abort` handle. When `abort()` is called the
|
|
1204
|
+
* `AbortSignal` passed to the factory is fired, cancelling any in-flight operation.
|
|
1205
|
+
* The abort error is transformed by `onError` into a typed `Err`.
|
|
1206
|
+
*
|
|
1207
|
+
* If an outer signal is also present (passed at the call site), aborting it
|
|
1208
|
+
* propagates into the internal controller.
|
|
1209
|
+
*
|
|
1210
|
+
* @example
|
|
1211
|
+
* ```ts
|
|
1212
|
+
* const { task: req, abort } = TaskResult.abortable(
|
|
1213
|
+
* (signal) => fetch(`/users/${id}`, { signal }).then(r => r.json()),
|
|
1214
|
+
* String,
|
|
1215
|
+
* );
|
|
1216
|
+
*
|
|
1217
|
+
* const result = pipe(req, TaskResult.retry({ attempts: 3 }));
|
|
1218
|
+
*
|
|
1219
|
+
* onCancel(abort);
|
|
1220
|
+
* await result();
|
|
1221
|
+
* ```
|
|
1222
|
+
*/
|
|
1223
|
+
const abortable: <E, A>(factory: (signal: AbortSignal) => Promise<A>, onError: (e: unknown) => E) => {
|
|
1224
|
+
task: TaskResult<E, A>;
|
|
1225
|
+
abort: () => void;
|
|
1226
|
+
};
|
|
1193
1227
|
}
|
|
1194
1228
|
|
|
1195
1229
|
/**
|