@nlozgachev/pipelined 0.19.0 → 0.21.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
@@ -1,6 +1,6 @@
1
1
  # pipelined
2
2
 
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)[![TypeScript](https://img.shields.io/badge/-0?style=for-the-badge&color=000&logo=typescript&label&logoColor=fff)](https://www.typescriptlang.org)
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) [![TypeScript](https://img.shields.io/badge/-0?style=for-the-badge&color=000&logo=typescript&label&logoColor=fff)](https://www.typescriptlang.org)
4
4
 
5
5
  Opinionated functional abstractions for TypeScript.
6
6
 
@@ -10,15 +10,99 @@ Opinionated functional abstractions for TypeScript.
10
10
  npm add @nlozgachev/pipelined
11
11
  ```
12
12
 
13
- ## What is this?
13
+ ## Possibly maybe
14
14
 
15
- A toolkit for expressing uncertainty precisely. Instead of `T | null`, `try/catch`, and loading
16
- state flag soup, you get types that name every possible state and make invalid ones unrepresentable.
17
- Each type comes with a consistent set of operations `map`, `chain`, `match`, `getOrElse` that
18
- compose with `pipe` and `flow`.
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
+ A careful, production-minded attempt at "fetch with retry, timeout, and cancellation":
27
+
28
+ ```ts
29
+ type UserResult =
30
+ | { ok: true; user: User; }
31
+ | { ok: false; error: "Timeout" | "NetworkError"; };
32
+
33
+ async function fetchUser(
34
+ id: string,
35
+ signal?: AbortSignal,
36
+ ): Promise<UserResult> {
37
+ async function attempt(n: number): Promise<UserResult> {
38
+ const controller = new AbortController();
39
+ const timerId = setTimeout(() => controller.abort(), 5000);
40
+ signal?.addEventListener("abort", () => controller.abort(), { once: true });
41
+
42
+ try {
43
+ const res = await fetch(`/users/${id}`, { signal: controller.signal });
44
+ clearTimeout(timerId);
45
+ return { ok: true, user: await res.json() };
46
+ } catch (e) {
47
+ clearTimeout(timerId);
48
+ if ((e as Error).name === "AbortError" && !signal?.aborted) {
49
+ return { ok: false, error: "Timeout" };
50
+ }
51
+ if (n < 3) {
52
+ await new Promise((r) => setTimeout(r, n * 1000));
53
+ return attempt(n + 1);
54
+ }
55
+ return { ok: false, error: "NetworkError" };
56
+ }
57
+ }
58
+ return attempt(1);
59
+ }
60
+ ```
61
+
62
+ The signal is forwarded by hand. The timeout needs its own controller. Timed-out aborts are
63
+ distinguished from external cancellation by checking `signal?.aborted`. The retry is recursive
64
+ to thread the attempt count.
65
+
66
+ With **pipelined**:
67
+
68
+ ```ts
69
+ import { pipe } from "@nlozgachev/pipelined/composition";
70
+ import { Result, TaskResult } from "@nlozgachev/pipelined/core";
71
+
72
+ const fetchUser = (id: string): TaskResult<ApiError, User> =>
73
+ pipe(
74
+ TaskResult.tryCatch(
75
+ (signal) => fetch(`/users/${id}`, { signal }).then((r) => r.json()),
76
+ (e) => new ApiError(e),
77
+ ),
78
+ TaskResult.timeout(5000, () => new ApiError("request timed out")),
79
+ TaskResult.retry({ attempts: 3, backoff: (n) => n * 1000 }),
80
+ );
81
+ ```
82
+
83
+ `TaskResult<ApiError, User>` is a lazy function — nothing runs until called. The `AbortSignal`
84
+ threads through every retry and the timeout automatically. The return type is the contract:
85
+ `ApiError` on the left, `User` on the right, nothing escapes as an exception.
86
+
87
+ ```ts
88
+ const controller = new AbortController();
89
+ const result = await fetchUser("42")(controller.signal);
90
+
91
+ if (Result.isOk(result)) {
92
+ render(result.value); // User
93
+ } else {
94
+ showError(result.error); // ApiError, not unknown
95
+ }
96
+ ```
19
97
 
20
98
  ## What's included?
21
99
 
100
+ `TaskResult` is one type. The library also covers the rest of the states you encounter in real
101
+ applications: values that may be absent, operations that accumulate multiple errors, data that moves
102
+ through `NotAsked >> Loading >> ( Success | Failure )`, nested immutable updates, and computations that
103
+ share a common environment. Every type follows the same conventions — `map`, `chain`, `match`,
104
+ `getOrElse` — so moving between them feels familiar.
105
+
22
106
  ### pipelined/core
23
107
 
24
108
  - **`Maybe<A>`** — a value that may not exist; propagates absence without null checks.
@@ -38,16 +122,22 @@ compose with `pipe` and `flow`.
38
122
  - **`Reader<R, A>`** — a computation that depends on an environment `R`, supplied once at the
39
123
  boundary.
40
124
 
41
-
42
125
  ### pipelined/utils
43
126
 
44
127
  Everyday utilities for built-in JS types.
45
128
 
46
129
  - **`Arr`** — array utilities, data-last, returning `Maybe` instead of `undefined`.
47
130
  - **`Rec`** — record/object utilities, data-last, with `Maybe`-returning key lookup.
131
+ - **`Dict`** — `ReadonlyMap<K, V>` utilities: `lookup`, `groupBy`, `upsert`, set operations.
132
+ - **`Uniq`** — `ReadonlySet<A>` utilities: `insert`, `remove`, `union`, `intersection`, `difference`.
48
133
  - **`Num`** — number utilities: `range`, `clamp`, `between`, safe `parse`, and curried arithmetic.
49
134
  - **`Str`** — string utilities: `split`, `trim`, `words`, `lines`, and safe `parse.int` / `parse.float`.
50
135
 
136
+ Every utility is benchmarked against its native equivalent. The data-last currying adds a function
137
+ call; that is the expected cost of composability. Operations that exceeded a reasonable overhead
138
+ have custom implementations that in several cases run faster than the native method they replace. See the
139
+ [benchmarks page](https://pipelined.lozgachev.dev/appendix/benchmarks) for the methodology.
140
+
51
141
  ### pipelined/types
52
142
 
53
143
  - **`Brand<K, T>`** — nominal typing at compile time, zero runtime cost.
@@ -58,34 +148,6 @@ Everyday utilities for built-in JS types.
58
148
  - **`pipe`**, **`flow`**, **`compose`** — function composition.
59
149
  - **`curry`** / **`uncurry`**, **`tap`**, **`memoize`**, and other function utilities.
60
150
 
61
- ## Example
62
-
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
-
85
- ## Documentation
86
-
87
- Full guides and API reference at **[pipelined.lozgachev.dev](https://pipelined.lozgachev.dev)**.
88
-
89
151
  ## License
90
152
 
91
153
  BSD-3-Clause
@@ -22,13 +22,17 @@ declare const _deferred: unique symbol;
22
22
  */
23
23
  type Deferred<A> = {
24
24
  readonly [_deferred]: A;
25
- readonly then: (onfulfilled: (value: A) => void) => void;
25
+ readonly then: (onfulfilled: (value: A) => unknown) => void;
26
26
  };
27
27
  declare namespace Deferred {
28
28
  /**
29
29
  * Wraps a `Promise` into a `Deferred`, structurally excluding rejection handlers,
30
30
  * `.catch()`, `.finally()`, and chainable `.then()`.
31
31
  *
32
+ * **Precondition**: `p` must never reject. If `p` rejects, the returned `Deferred` will
33
+ * never resolve — `await`-ing it will hang indefinitely. Use `TaskResult.tryCatch` to
34
+ * handle operations that may fail before converting to a `Deferred`.
35
+ *
32
36
  * @example
33
37
  * ```ts
34
38
  * const d = Deferred.fromPromise(Promise.resolve("hello"));
@@ -69,6 +73,76 @@ type WithSecond<T> = {
69
73
  type WithLog<T> = {
70
74
  readonly log: ReadonlyArray<T>;
71
75
  };
76
+ /** Retry policy for `Op.interpret`. */
77
+ type RetryOptions<E> = {
78
+ readonly attempts: number;
79
+ readonly backoff?: number | ((attempt: number) => number);
80
+ readonly when?: (error: E) => boolean;
81
+ };
82
+ /** Timeout policy for `Op.interpret`. Wraps the entire retry sequence. */
83
+ type TimeoutOptions<E> = {
84
+ readonly ms: number;
85
+ readonly onTimeout: () => E;
86
+ };
87
+ type WithRetry<E> = {
88
+ readonly retry: RetryOptions<E>;
89
+ };
90
+ type WithTimeout<E> = {
91
+ readonly timeout?: TimeoutOptions<E>;
92
+ };
93
+ type WithMs = {
94
+ readonly ms: number;
95
+ };
96
+ /** For `throttled`: also fire on the trailing edge after the cooldown. */
97
+ type WithTrailing = {
98
+ readonly trailing: true;
99
+ };
100
+ /** For `debounced`: also fire on the leading edge (first call). */
101
+ type WithLeading = {
102
+ readonly leading: true;
103
+ };
104
+ /** For `debounced`: maximum ms before the trailing call fires regardless of continued activity. */
105
+ type WithMaxWait = {
106
+ readonly maxWait: number;
107
+ };
108
+ type WithN = {
109
+ readonly n: number;
110
+ };
111
+ /**
112
+ * `O` is a string literal (or union of literals) representing the overflow value.
113
+ * The generic lets overload signatures discriminate on the specific value:
114
+ * `WithOverflow<"drop">` → `{ overflow: "drop" }`
115
+ * `WithOverflow<"replace-last">` → `{ overflow: "replace-last" }`
116
+ * Used by both `concurrent` (`"drop" | "queue"`) and `queue` (`"drop" | "replace-last"`).
117
+ * `extends string` prevents `WithOverflow<42>` from being valid.
118
+ */
119
+ type WithOverflow<O extends string> = {
120
+ readonly overflow: O;
121
+ };
122
+ type WithMaxSize = {
123
+ readonly maxSize: number;
124
+ };
125
+ type WithConcurrency = {
126
+ readonly concurrency?: number;
127
+ };
128
+ type WithDedupe<I> = {
129
+ readonly dedupe: (a: I, b: I) => boolean;
130
+ };
131
+ type WithSize = {
132
+ readonly size?: number;
133
+ };
134
+ type WithCooldown = {
135
+ readonly cooldown?: number;
136
+ };
137
+ type WithMinInterval = {
138
+ readonly minInterval?: number;
139
+ };
140
+ type WithKey<I, K> = {
141
+ readonly key: (input: I) => K;
142
+ };
143
+ type WithPerKey<S extends string> = {
144
+ readonly perKey: S;
145
+ };
72
146
 
73
147
  type Ok<A> = WithKind<"Ok"> & WithValue<A>;
74
148
  type Err<E> = WithKind<"Error"> & WithError<E>;
@@ -465,6 +539,10 @@ declare namespace Maybe {
465
539
  * - **Infallible** — it never rejects. If failure is possible, encode it in the
466
540
  * return type using `TaskResult<E, A>` instead.
467
541
  *
542
+ * An optional `AbortSignal` can be passed at the call site. Combinators like
543
+ * `retry`, `pollUntil`, and `timeout` thread it automatically to every inner
544
+ * operation. Existing tasks that ignore the signal continue to work unchanged.
545
+ *
468
546
  * Calling a Task returns a `Deferred<A>` — a one-shot async value that supports
469
547
  * `await` but has no `.catch()`, `.finally()`, or chainable `.then()`.
470
548
  *
@@ -495,7 +573,7 @@ declare namespace Maybe {
495
573
  * const result = await formatted();
496
574
  * ```
497
575
  */
498
- type Task<A> = () => Deferred<A>;
576
+ type Task<A> = (signal?: AbortSignal) => Deferred<A>;
499
577
  declare namespace Task {
500
578
  /**
501
579
  * Creates a Task that immediately resolves to the given value.
@@ -509,13 +587,14 @@ declare namespace Task {
509
587
  const resolve: <A>(value: A) => Task<A>;
510
588
  /**
511
589
  * Creates a Task from a function that returns a Promise.
590
+ * The factory optionally receives an `AbortSignal` forwarded from the call site.
512
591
  *
513
592
  * @example
514
593
  * ```ts
515
594
  * const getTimestamp = Task.from(() => Promise.resolve(Date.now()));
516
595
  * ```
517
596
  */
518
- const from: <A>(f: () => Promise<A>) => Task<A>;
597
+ const from: <A>(f: (signal?: AbortSignal) => Promise<A>) => Task<A>;
519
598
  /**
520
599
  * Transforms the value inside a Task.
521
600
  *
@@ -660,7 +739,9 @@ declare namespace Task {
660
739
  const sequential: <A>(tasks: ReadonlyArray<Task<A>>) => Task<ReadonlyArray<A>>;
661
740
  /**
662
741
  * Converts a `Task<A>` into a `Task<Result<E, A>>`, resolving to `Err` if the
663
- * Task does not complete within the given time.
742
+ * Task does not complete within the given time. The inner Task receives an
743
+ * `AbortSignal` that fires when the deadline passes, so operations like `fetch`
744
+ * that accept a signal are cancelled rather than left dangling.
664
745
  *
665
746
  * @example
666
747
  * ```ts
@@ -672,6 +753,28 @@ declare namespace Task {
672
753
  * ```
673
754
  */
674
755
  const timeout: <E>(ms: number, onTimeout: () => E) => <A>(task: Task<A>) => Task<Result<E, A>>;
756
+ /**
757
+ * Creates a Task paired with an `abort` handle. When `abort()` is called the
758
+ * `AbortSignal` passed to the factory is fired, cancelling any in-flight
759
+ * operation (e.g. a `fetch`) immediately.
760
+ *
761
+ * If an outer signal is also present (passed at the call site), aborting it
762
+ * propagates into the internal controller.
763
+ *
764
+ * @example
765
+ * ```ts
766
+ * const { task: poll, abort } = Task.abortable(
767
+ * (signal) => waitForEvent(bus, "ready", { signal }),
768
+ * );
769
+ *
770
+ * onUnmount(abort);
771
+ * await poll();
772
+ * ```
773
+ */
774
+ const abortable: <A>(factory: (signal: AbortSignal) => Promise<A>) => {
775
+ task: Task<A>;
776
+ abort: () => void;
777
+ };
675
778
  }
676
779
 
677
- 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 };
780
+ 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 RetryOptions as d, type TimeoutOptions as e, type WithMs as f, type WithTrailing as g, type WithRetry as h, type WithTimeout as i, type WithLeading as j, type WithMaxWait as k, type WithN as l, type WithOverflow as m, type WithKey as n, type WithPerKey as o, type WithMinInterval as p, type WithCooldown as q, type WithMaxSize as r, type WithDedupe as s, type WithConcurrency as t, type WithSize as u, type WithErrors as v, type WithFirst as w, type WithSecond as x };
@@ -22,13 +22,17 @@ declare const _deferred: unique symbol;
22
22
  */
23
23
  type Deferred<A> = {
24
24
  readonly [_deferred]: A;
25
- readonly then: (onfulfilled: (value: A) => void) => void;
25
+ readonly then: (onfulfilled: (value: A) => unknown) => void;
26
26
  };
27
27
  declare namespace Deferred {
28
28
  /**
29
29
  * Wraps a `Promise` into a `Deferred`, structurally excluding rejection handlers,
30
30
  * `.catch()`, `.finally()`, and chainable `.then()`.
31
31
  *
32
+ * **Precondition**: `p` must never reject. If `p` rejects, the returned `Deferred` will
33
+ * never resolve — `await`-ing it will hang indefinitely. Use `TaskResult.tryCatch` to
34
+ * handle operations that may fail before converting to a `Deferred`.
35
+ *
32
36
  * @example
33
37
  * ```ts
34
38
  * const d = Deferred.fromPromise(Promise.resolve("hello"));
@@ -69,6 +73,76 @@ type WithSecond<T> = {
69
73
  type WithLog<T> = {
70
74
  readonly log: ReadonlyArray<T>;
71
75
  };
76
+ /** Retry policy for `Op.interpret`. */
77
+ type RetryOptions<E> = {
78
+ readonly attempts: number;
79
+ readonly backoff?: number | ((attempt: number) => number);
80
+ readonly when?: (error: E) => boolean;
81
+ };
82
+ /** Timeout policy for `Op.interpret`. Wraps the entire retry sequence. */
83
+ type TimeoutOptions<E> = {
84
+ readonly ms: number;
85
+ readonly onTimeout: () => E;
86
+ };
87
+ type WithRetry<E> = {
88
+ readonly retry: RetryOptions<E>;
89
+ };
90
+ type WithTimeout<E> = {
91
+ readonly timeout?: TimeoutOptions<E>;
92
+ };
93
+ type WithMs = {
94
+ readonly ms: number;
95
+ };
96
+ /** For `throttled`: also fire on the trailing edge after the cooldown. */
97
+ type WithTrailing = {
98
+ readonly trailing: true;
99
+ };
100
+ /** For `debounced`: also fire on the leading edge (first call). */
101
+ type WithLeading = {
102
+ readonly leading: true;
103
+ };
104
+ /** For `debounced`: maximum ms before the trailing call fires regardless of continued activity. */
105
+ type WithMaxWait = {
106
+ readonly maxWait: number;
107
+ };
108
+ type WithN = {
109
+ readonly n: number;
110
+ };
111
+ /**
112
+ * `O` is a string literal (or union of literals) representing the overflow value.
113
+ * The generic lets overload signatures discriminate on the specific value:
114
+ * `WithOverflow<"drop">` → `{ overflow: "drop" }`
115
+ * `WithOverflow<"replace-last">` → `{ overflow: "replace-last" }`
116
+ * Used by both `concurrent` (`"drop" | "queue"`) and `queue` (`"drop" | "replace-last"`).
117
+ * `extends string` prevents `WithOverflow<42>` from being valid.
118
+ */
119
+ type WithOverflow<O extends string> = {
120
+ readonly overflow: O;
121
+ };
122
+ type WithMaxSize = {
123
+ readonly maxSize: number;
124
+ };
125
+ type WithConcurrency = {
126
+ readonly concurrency?: number;
127
+ };
128
+ type WithDedupe<I> = {
129
+ readonly dedupe: (a: I, b: I) => boolean;
130
+ };
131
+ type WithSize = {
132
+ readonly size?: number;
133
+ };
134
+ type WithCooldown = {
135
+ readonly cooldown?: number;
136
+ };
137
+ type WithMinInterval = {
138
+ readonly minInterval?: number;
139
+ };
140
+ type WithKey<I, K> = {
141
+ readonly key: (input: I) => K;
142
+ };
143
+ type WithPerKey<S extends string> = {
144
+ readonly perKey: S;
145
+ };
72
146
 
73
147
  type Ok<A> = WithKind<"Ok"> & WithValue<A>;
74
148
  type Err<E> = WithKind<"Error"> & WithError<E>;
@@ -465,6 +539,10 @@ declare namespace Maybe {
465
539
  * - **Infallible** — it never rejects. If failure is possible, encode it in the
466
540
  * return type using `TaskResult<E, A>` instead.
467
541
  *
542
+ * An optional `AbortSignal` can be passed at the call site. Combinators like
543
+ * `retry`, `pollUntil`, and `timeout` thread it automatically to every inner
544
+ * operation. Existing tasks that ignore the signal continue to work unchanged.
545
+ *
468
546
  * Calling a Task returns a `Deferred<A>` — a one-shot async value that supports
469
547
  * `await` but has no `.catch()`, `.finally()`, or chainable `.then()`.
470
548
  *
@@ -495,7 +573,7 @@ declare namespace Maybe {
495
573
  * const result = await formatted();
496
574
  * ```
497
575
  */
498
- type Task<A> = () => Deferred<A>;
576
+ type Task<A> = (signal?: AbortSignal) => Deferred<A>;
499
577
  declare namespace Task {
500
578
  /**
501
579
  * Creates a Task that immediately resolves to the given value.
@@ -509,13 +587,14 @@ declare namespace Task {
509
587
  const resolve: <A>(value: A) => Task<A>;
510
588
  /**
511
589
  * Creates a Task from a function that returns a Promise.
590
+ * The factory optionally receives an `AbortSignal` forwarded from the call site.
512
591
  *
513
592
  * @example
514
593
  * ```ts
515
594
  * const getTimestamp = Task.from(() => Promise.resolve(Date.now()));
516
595
  * ```
517
596
  */
518
- const from: <A>(f: () => Promise<A>) => Task<A>;
597
+ const from: <A>(f: (signal?: AbortSignal) => Promise<A>) => Task<A>;
519
598
  /**
520
599
  * Transforms the value inside a Task.
521
600
  *
@@ -660,7 +739,9 @@ declare namespace Task {
660
739
  const sequential: <A>(tasks: ReadonlyArray<Task<A>>) => Task<ReadonlyArray<A>>;
661
740
  /**
662
741
  * Converts a `Task<A>` into a `Task<Result<E, A>>`, resolving to `Err` if the
663
- * Task does not complete within the given time.
742
+ * Task does not complete within the given time. The inner Task receives an
743
+ * `AbortSignal` that fires when the deadline passes, so operations like `fetch`
744
+ * that accept a signal are cancelled rather than left dangling.
664
745
  *
665
746
  * @example
666
747
  * ```ts
@@ -672,6 +753,28 @@ declare namespace Task {
672
753
  * ```
673
754
  */
674
755
  const timeout: <E>(ms: number, onTimeout: () => E) => <A>(task: Task<A>) => Task<Result<E, A>>;
756
+ /**
757
+ * Creates a Task paired with an `abort` handle. When `abort()` is called the
758
+ * `AbortSignal` passed to the factory is fired, cancelling any in-flight
759
+ * operation (e.g. a `fetch`) immediately.
760
+ *
761
+ * If an outer signal is also present (passed at the call site), aborting it
762
+ * propagates into the internal controller.
763
+ *
764
+ * @example
765
+ * ```ts
766
+ * const { task: poll, abort } = Task.abortable(
767
+ * (signal) => waitForEvent(bus, "ready", { signal }),
768
+ * );
769
+ *
770
+ * onUnmount(abort);
771
+ * await poll();
772
+ * ```
773
+ */
774
+ const abortable: <A>(factory: (signal: AbortSignal) => Promise<A>) => {
775
+ task: Task<A>;
776
+ abort: () => void;
777
+ };
675
778
  }
676
779
 
677
- 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 };
780
+ 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 RetryOptions as d, type TimeoutOptions as e, type WithMs as f, type WithTrailing as g, type WithRetry as h, type WithTimeout as i, type WithLeading as j, type WithMaxWait as k, type WithN as l, type WithOverflow as m, type WithKey as n, type WithPerKey as o, type WithMinInterval as p, type WithCooldown as q, type WithMaxSize as r, type WithDedupe as s, type WithConcurrency as t, type WithSize as u, type WithErrors as v, type WithFirst as w, type WithSecond as x };
@@ -1,13 +1,11 @@
1
1
  // src/Core/Deferred.ts
2
- var _store = /* @__PURE__ */ new WeakMap();
3
2
  var Deferred;
4
3
  ((Deferred2) => {
5
- Deferred2.fromPromise = (p) => {
6
- const d = { then: ((f) => p.then(f)) };
7
- _store.set(d, p);
8
- return d;
9
- };
10
- Deferred2.toPromise = (d) => _store.get(d) ?? new Promise((resolve) => d.then(resolve));
4
+ Deferred2.fromPromise = (p) => (
5
+ // eslint-disable-next-line unicorn/no-thenable -- Deferred is intentionally thenable; it is the mechanism that makes Task awaitable
6
+ { then: ((f) => p.then(f)) }
7
+ );
8
+ Deferred2.toPromise = (d) => new Promise((resolve) => d.then(resolve));
11
9
  })(Deferred || (Deferred = {}));
12
10
 
13
11
  // src/Core/Maybe.ts
@@ -69,77 +67,99 @@ var Result;
69
67
  })(Result || (Result = {}));
70
68
 
71
69
  // src/Core/Task.ts
72
- var toPromise = (task) => Deferred.toPromise(task());
70
+ var toPromise = (task, signal) => Deferred.toPromise(task(signal));
73
71
  var Task;
74
72
  ((Task2) => {
75
73
  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))));
74
+ Task2.from = (f) => (signal) => Deferred.fromPromise(f(signal));
75
+ Task2.map = (f) => (data) => (0, Task2.from)((signal) => toPromise(data, signal).then(f));
76
+ Task2.chain = (f) => (data) => (0, Task2.from)((signal) => toPromise(data, signal).then((a) => toPromise(f(a), signal)));
79
77
  Task2.ap = (arg) => (data) => (0, Task2.from)(
80
- () => Promise.all([
81
- toPromise(data),
82
- toPromise(arg)
78
+ (signal) => Promise.all([
79
+ toPromise(data, signal),
80
+ toPromise(arg, signal)
83
81
  ]).then(([f, a]) => f(a))
84
82
  );
85
83
  Task2.tap = (f) => (data) => (0, Task2.from)(
86
- () => toPromise(data).then((a) => {
84
+ (signal) => toPromise(data, signal).then((a) => {
87
85
  f(a);
88
86
  return a;
89
87
  })
90
88
  );
91
89
  Task2.all = (tasks) => (0, Task2.from)(
92
- () => Promise.all(tasks.map((t) => toPromise(t)))
90
+ (signal) => Promise.all(tasks.map((t) => toPromise(t, signal)))
93
91
  );
94
92
  Task2.delay = (ms) => (data) => (0, Task2.from)(
95
- () => new Promise(
93
+ (signal) => new Promise(
96
94
  (resolve2) => setTimeout(
97
- () => toPromise(data).then(resolve2),
95
+ () => toPromise(data, signal).then(resolve2),
98
96
  ms
99
97
  )
100
98
  )
101
99
  );
102
- Task2.repeat = (options) => (task) => (0, Task2.from)(() => {
100
+ Task2.repeat = (options) => (task) => (0, Task2.from)((signal) => {
103
101
  const { times, delay: ms } = options;
104
102
  if (times <= 0) return Promise.resolve([]);
105
103
  const results = [];
106
104
  const wait = () => ms !== void 0 && ms > 0 ? new Promise((r) => setTimeout(r, ms)) : Promise.resolve();
107
- const run = (left) => toPromise(task).then((a) => {
105
+ const run = (left) => toPromise(task, signal).then((a) => {
108
106
  results.push(a);
109
107
  if (left <= 1) return results;
110
108
  return wait().then(() => run(left - 1));
111
109
  });
112
110
  return run(times);
113
111
  });
114
- Task2.repeatUntil = (options) => (task) => (0, Task2.from)(() => {
112
+ Task2.repeatUntil = (options) => (task) => (0, Task2.from)((signal) => {
115
113
  const { when: predicate, delay: ms } = options;
116
114
  const wait = () => ms !== void 0 && ms > 0 ? new Promise((r) => setTimeout(r, ms)) : Promise.resolve();
117
- const run = () => toPromise(task).then((a) => {
115
+ const run = () => toPromise(task, signal).then((a) => {
118
116
  if (predicate(a)) return a;
119
117
  return wait().then(run);
120
118
  });
121
119
  return run();
122
120
  });
123
- Task2.race = (tasks) => (0, Task2.from)(() => Promise.race(tasks.map(toPromise)));
124
- Task2.sequential = (tasks) => (0, Task2.from)(async () => {
121
+ Task2.race = (tasks) => (0, Task2.from)((signal) => Promise.race(tasks.map((t) => toPromise(t, signal))));
122
+ Task2.sequential = (tasks) => (0, Task2.from)(async (signal) => {
125
123
  const results = [];
126
124
  for (const task of tasks) {
127
- results.push(await toPromise(task));
125
+ results.push(await toPromise(task, signal));
128
126
  }
129
127
  return results;
130
128
  });
131
- Task2.timeout = (ms, onTimeout) => (task) => (0, Task2.from)(() => {
129
+ Task2.timeout = (ms, onTimeout) => (task) => (0, Task2.from)((outerSignal) => {
130
+ const controller = new AbortController();
131
+ const onOuterAbort = () => controller.abort();
132
+ outerSignal?.addEventListener("abort", onOuterAbort, { once: true });
132
133
  let timerId;
133
134
  return Promise.race([
134
- toPromise(task).then((a) => {
135
+ toPromise(task, controller.signal).then((a) => {
135
136
  clearTimeout(timerId);
137
+ outerSignal?.removeEventListener("abort", onOuterAbort);
136
138
  return Result.ok(a);
137
139
  }),
138
140
  new Promise((resolve2) => {
139
- timerId = setTimeout(() => resolve2(Result.err(onTimeout())), ms);
141
+ timerId = setTimeout(() => {
142
+ controller.abort();
143
+ outerSignal?.removeEventListener("abort", onOuterAbort);
144
+ resolve2(Result.err(onTimeout()));
145
+ }, ms);
140
146
  })
141
147
  ]);
142
148
  });
149
+ Task2.abortable = (factory) => {
150
+ const controller = new AbortController();
151
+ const task = (outerSignal) => {
152
+ if (outerSignal) {
153
+ if (outerSignal.aborted) {
154
+ controller.abort(outerSignal.reason);
155
+ } else {
156
+ outerSignal.addEventListener("abort", () => controller.abort(outerSignal.reason), { once: true });
157
+ }
158
+ }
159
+ return Deferred.fromPromise(factory(controller.signal));
160
+ };
161
+ return { task, abort: () => controller.abort() };
162
+ };
143
163
  })(Task || (Task = {}));
144
164
 
145
165
  export {