@nlozgachev/pipelined 0.20.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
 
@@ -23,45 +23,61 @@ Full guides and API reference at **[pipelined.lozgachev.dev](https://pipelined.l
23
23
 
24
24
  ## Example
25
25
 
26
- The standard approach to "fetch with retry and timeout":
26
+ A careful, production-minded attempt at "fetch with retry, timeout, and cancellation":
27
27
 
28
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;
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);
43
59
  }
44
60
  ```
45
61
 
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.
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.
49
65
 
50
66
  With **pipelined**:
51
67
 
52
68
  ```ts
53
- import { TaskResult } from "@nlozgachev/pipelined/core";
54
69
  import { pipe } from "@nlozgachev/pipelined/composition";
70
+ import { Result, TaskResult } from "@nlozgachev/pipelined/core";
55
71
 
56
72
  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
- );
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
+ );
65
81
  ```
66
82
 
67
83
  `TaskResult<ApiError, User>` is a lazy function — nothing runs until called. The `AbortSignal`
@@ -73,9 +89,9 @@ const controller = new AbortController();
73
89
  const result = await fetchUser("42")(controller.signal);
74
90
 
75
91
  if (Result.isOk(result)) {
76
- render(result.value); // User
92
+ render(result.value); // User
77
93
  } else {
78
- showError(result.error.message); // ApiError, not unknown
94
+ showError(result.error); // ApiError, not unknown
79
95
  }
80
96
  ```
81
97
 
@@ -106,7 +122,6 @@ share a common environment. Every type follows the same conventions — `map`, `
106
122
  - **`Reader<R, A>`** — a computation that depends on an environment `R`, supplied once at the
107
123
  boundary.
108
124
 
109
-
110
125
  ### pipelined/utils
111
126
 
112
127
  Everyday utilities for built-in JS types.
@@ -133,9 +148,6 @@ have custom implementations that in several cases run faster than the native met
133
148
  - **`pipe`**, **`flow`**, **`compose`** — function composition.
134
149
  - **`curry`** / **`uncurry`**, **`tap`**, **`memoize`**, and other function utilities.
135
150
 
136
-
137
-
138
-
139
151
  ## License
140
152
 
141
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>;
@@ -703,4 +777,4 @@ declare namespace Task {
703
777
  };
704
778
  }
705
779
 
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 };
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>;
@@ -703,4 +777,4 @@ declare namespace Task {
703
777
  };
704
778
  }
705
779
 
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 };
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
@@ -3,7 +3,7 @@ import {
3
3
  Maybe,
4
4
  Result,
5
5
  Task
6
- } from "./chunk-RUDOUVQR.mjs";
6
+ } from "./chunk-2DPG2RDB.mjs";
7
7
  import {
8
8
  isNonEmptyList
9
9
  } from "./chunk-DBIC62UV.mjs";
@@ -364,7 +364,7 @@ var Rec;
364
364
  const vals = Object.values(data);
365
365
  const result = {};
366
366
  for (let i = 0; i < keys2.length; i++) {
367
- result[keys2[i]] = f(vals[i]);
367
+ Object.defineProperty(result, keys2[i], { value: f(vals[i]), writable: true, enumerable: true, configurable: true });
368
368
  }
369
369
  return result;
370
370
  };
@@ -373,21 +373,30 @@ var Rec;
373
373
  const vals = Object.values(data);
374
374
  const result = {};
375
375
  for (let i = 0; i < keys2.length; i++) {
376
- result[keys2[i]] = f(keys2[i], vals[i]);
376
+ Object.defineProperty(result, keys2[i], {
377
+ value: f(keys2[i], vals[i]),
378
+ writable: true,
379
+ enumerable: true,
380
+ configurable: true
381
+ });
377
382
  }
378
383
  return result;
379
384
  };
380
385
  Rec2.filter = (predicate) => (data) => {
381
386
  const result = {};
382
387
  for (const [k, v] of Object.entries(data)) {
383
- if (predicate(v)) result[k] = v;
388
+ if (predicate(v)) {
389
+ Object.defineProperty(result, k, { value: v, writable: true, enumerable: true, configurable: true });
390
+ }
384
391
  }
385
392
  return result;
386
393
  };
387
394
  Rec2.filterWithKey = (predicate) => (data) => {
388
395
  const result = {};
389
396
  for (const [k, v] of Object.entries(data)) {
390
- if (predicate(k, v)) result[k] = v;
397
+ if (predicate(k, v)) {
398
+ Object.defineProperty(result, k, { value: v, writable: true, enumerable: true, configurable: true });
399
+ }
391
400
  }
392
401
  return result;
393
402
  };
@@ -400,8 +409,11 @@ var Rec;
400
409
  const result = {};
401
410
  for (const item of items) {
402
411
  const key = keyFn(item);
403
- if (key in result) result[key].push(item);
404
- else result[key] = [item];
412
+ if (Object.hasOwn(result, key)) {
413
+ result[key].push(item);
414
+ } else {
415
+ Object.defineProperty(result, key, { value: [item], writable: true, enumerable: true, configurable: true });
416
+ }
405
417
  }
406
418
  return result;
407
419
  };
@@ -409,7 +421,7 @@ var Rec;
409
421
  const result = {};
410
422
  for (const key of pickedKeys) {
411
423
  if (Object.hasOwn(data, key)) {
412
- result[key] = data[key];
424
+ Object.defineProperty(result, key, { value: data[key], writable: true, enumerable: true, configurable: true });
413
425
  }
414
426
  }
415
427
  return result;
@@ -419,7 +431,12 @@ var Rec;
419
431
  const result = {};
420
432
  for (const key of Object.keys(data)) {
421
433
  if (!omitSet.has(key)) {
422
- result[key] = data[key];
434
+ Object.defineProperty(result, key, {
435
+ value: data[key],
436
+ writable: true,
437
+ enumerable: true,
438
+ configurable: true
439
+ });
423
440
  }
424
441
  }
425
442
  return result;
@@ -433,14 +450,16 @@ var Rec;
433
450
  Rec2.mapKeys = (f) => (data) => {
434
451
  const result = {};
435
452
  for (const [k, v] of Object.entries(data)) {
436
- result[f(k)] = v;
453
+ Object.defineProperty(result, f(k), { value: v, writable: true, enumerable: true, configurable: true });
437
454
  }
438
455
  return result;
439
456
  };
440
457
  Rec2.compact = (data) => {
441
458
  const result = {};
442
459
  for (const [k, v] of Object.entries(data)) {
443
- if (v.kind === "Some") result[k] = v.value;
460
+ if (v.kind === "Some") {
461
+ Object.defineProperty(result, k, { value: v.value, writable: true, enumerable: true, configurable: true });
462
+ }
444
463
  }
445
464
  return result;
446
465
  };