@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 +97 -35
- package/dist/{Task-DBW4nOZR.d.mts → Task-CT8iwwuB.d.mts} +108 -5
- package/dist/{Task-DUdIQm-Q.d.ts → Task-GSGtQO1m.d.ts} +108 -5
- package/dist/{chunk-EAR4TIGH.mjs → chunk-2DPG2RDB.mjs} +48 -28
- package/dist/{chunk-B3YNH6GZ.mjs → chunk-C3Z56PCR.mjs} +30 -11
- package/dist/chunk-SSZXZTIX.mjs +1398 -0
- package/dist/core.d.mts +603 -77
- package/dist/core.d.ts +603 -77
- package/dist/core.js +926 -67
- package/dist/core.mjs +4 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +955 -77
- package/dist/index.mjs +5 -3
- package/dist/utils.d.mts +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +77 -38
- package/dist/utils.mjs +2 -2
- package/package.json +3 -2
- package/dist/chunk-UWGFO7BH.mjs +0 -560
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pipelined
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@nlozgachev/pipelined)[](https://github.com/nlozgachev/pipelined/actions/workflows/publish.yml)[](https://app.codecov.io/github/nlozgachev/pipelined)[](https://www.typescriptlang.org)
|
|
3
|
+
[](https://www.npmjs.com/package/@nlozgachev/pipelined) [](https://github.com/nlozgachev/pipelined/actions/workflows/publish.yml) [](https://app.codecov.io/github/nlozgachev/pipelined) [](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
|
-
##
|
|
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
|
+
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) =>
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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(() =>
|
|
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 {
|