@nlozgachev/pipelined 0.18.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 CHANGED
@@ -10,15 +10,83 @@ 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
+ 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,51 +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 { TaskResult } from "@nlozgachev/pipelined/core";
65
- import { pipe } from "@nlozgachev/pipelined/composition";
66
137
 
67
- // Typed errors. Lazy. Composable.
68
- const getUser = (id: string): TaskResult<string, User> =>
69
- pipe(
70
- TaskResult.tryCatch(
71
- () => fetch(`/users/${id}`).then((r) => r.json() as Promise<User>),
72
- (e) => `fetch failed: ${e}`,
73
- ),
74
- TaskResult.timeout(5_000, () => "request timed out"),
75
- TaskResult.retry({ attempts: 3, backoff: (n) => n * 1_000 }),
76
- );
77
-
78
- // Poll until a background job finishes — stop immediately on failure
79
- const waitForExport = (jobId: string): TaskResult<string, ExportResult> =>
80
- pipe(
81
- TaskResult.tryCatch(
82
- () => fetch(`/exports/${jobId}`).then((r) => r.json() as Promise<Job>),
83
- String,
84
- ),
85
- TaskResult.pollUntil({
86
- when: (job) => job.status === "done",
87
- delay: (n) => n * 500, // 500 ms, 1 s, 1.5 s, ...
88
- }),
89
- TaskResult.map((job) => job.result),
90
- );
91
-
92
- // Compose the two — nothing runs until the final call
93
- const message = await pipe(
94
- getUser("abc"),
95
- TaskResult.chain((user) => waitForExport(user.exportId)),
96
- TaskResult.match({
97
- ok: (r) => `Export ready: ${r.url}`,
98
- err: (e) => `Failed: ${e}`,
99
- }),
100
- )();
101
- ```
102
-
103
- ## Documentation
104
-
105
- Full guides and API reference at **[pipelined.lozgachev.dev](https://pipelined.lozgachev.dev)**.
106
138
 
107
139
  ## License
108
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-EAR4TIGH.mjs";
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 > 0 ? new Promise((r) => setTimeout(r, ms)) : Promise.resolve()).then(() => run(left - 1));
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 > 0 ? new Promise((r) => setTimeout(r, ms)) : Promise.resolve()).then(() => run(attempt + 1));
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(() => resolve(Result.err(onTimeout())), ms);
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
@@ -3,7 +3,7 @@ import {
3
3
  Maybe,
4
4
  Result,
5
5
  Task
6
- } from "./chunk-EAR4TIGH.mjs";
6
+ } from "./chunk-RUDOUVQR.mjs";
7
7
  import {
8
8
  isNonEmptyList
9
9
  } from "./chunk-DBIC62UV.mjs";
@@ -166,8 +166,7 @@ var on = (f, g) => (a, b) => f(g(a), g(b));
166
166
 
167
167
  // src/Composition/pipe.ts
168
168
  function pipe(a, ab, bc, cd, de, ef, fg, gh, hi, ij, jk) {
169
- const len = arguments.length;
170
- switch (len) {
169
+ switch (arguments.length) {
171
170
  case 1:
172
171
  return a;
173
172
  case 2:
@@ -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(() => resolve2(Result.err(onTimeout())), ms);
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 {
@@ -218,8 +218,7 @@ var on = (f, g) => (a, b) => f(g(a), g(b));
218
218
 
219
219
  // src/Composition/pipe.ts
220
220
  function pipe(a, ab, bc, cd, de, ef, fg, gh, hi, ij, jk) {
221
- const len = arguments.length;
222
- switch (len) {
221
+ switch (arguments.length) {
223
222
  case 1:
224
223
  return a;
225
224
  case 2:
@@ -26,7 +26,7 @@ import {
26
26
  uncurry,
27
27
  uncurry3,
28
28
  uncurry4
29
- } from "./chunk-5HMYR4XB.mjs";
29
+ } from "./chunk-NRF2FVPZ.mjs";
30
30
  export {
31
31
  and,
32
32
  compose,