@nlozgachev/pipelined 0.50.0 → 0.52.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 +223 -68
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -15,11 +15,13 @@ npm add @nlozgachev/pipelined
|
|
|
15
15
|
## Possibly maybe
|
|
16
16
|
|
|
17
17
|
In mainstream TypeScript, code is often burdened by implicit control flow: unchecked exceptions,
|
|
18
|
-
manual null propagation,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
manual null propagation, unhandled asynchronous failures, UI state race conditions, and deeply
|
|
19
|
+
nested spread operators. `pipelined` turns these complex runtime states into transparent, composable
|
|
20
|
+
data structures.
|
|
21
|
+
|
|
22
|
+
By representing optionality as `Maybe`, failures as `Result`, multi-field errors as `Validation`,
|
|
23
|
+
lazy asynchronous workflows as `Task.Result`, request concurrency as `Op`, and event pipelines as
|
|
24
|
+
`Stream`, the library disentangles business logic from control mechanics.
|
|
23
25
|
|
|
24
26
|
To support these patterns without introducing bloat, the library is designed to be lightweight,
|
|
25
27
|
zero-dependency, and fully tree-shakeable. The core module (`/core`) is under 16 KB gzipped, and the
|
|
@@ -30,7 +32,11 @@ environments.
|
|
|
30
32
|
|
|
31
33
|
Full guides and API reference at **[pipelined.lozgachev.dev](https://pipelined.lozgachev.dev)**.
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Examples
|
|
38
|
+
|
|
39
|
+
### Composing optional values
|
|
34
40
|
|
|
35
41
|
`null` checks accumulate fast. Each one is a conditional branch that the type system can't help you
|
|
36
42
|
forget. `Maybe<A>` turns absence into a value that composes — the same operations apply whether or
|
|
@@ -45,7 +51,7 @@ const parseDiscount = (raw: string): string =>
|
|
|
45
51
|
pipe(
|
|
46
52
|
raw,
|
|
47
53
|
Str.trim,
|
|
48
|
-
Num.parse, // "
|
|
54
|
+
Num.parse, // "15" → Some(15), "abc" → None
|
|
49
55
|
Maybe.filter((n) => n >= 0 && n <= 100), // out of range → None
|
|
50
56
|
Maybe.map((n) => `${n}% off`),
|
|
51
57
|
Maybe.getOrElse(() => "No discount"),
|
|
@@ -58,7 +64,7 @@ parseDiscount("abc"); // "No discount"
|
|
|
58
64
|
|
|
59
65
|
Every step that sees `None` is skipped. The fallback runs once, at the end.
|
|
60
66
|
|
|
61
|
-
|
|
67
|
+
### Typed async errors and cancellation
|
|
62
68
|
|
|
63
69
|
In JavaScript, asynchronous exceptions bypass the static type system, leaving unhandled rejections
|
|
64
70
|
as invisible runtime risks. `Task.Result<E, A>` represents fallible asynchronous computations as
|
|
@@ -69,6 +75,8 @@ function signature, ensuring that failures are handled before compile time:
|
|
|
69
75
|
import { pipe } from "@nlozgachev/pipelined/composition";
|
|
70
76
|
import { Result, Task } from "@nlozgachev/pipelined/core";
|
|
71
77
|
|
|
78
|
+
type User = { id: string; name: string };
|
|
79
|
+
type Post = { id: string; title: string };
|
|
72
80
|
type ApiError = { status: number; message: string };
|
|
73
81
|
|
|
74
82
|
const fetchUser = (id: string): Task.Result<ApiError, User> =>
|
|
@@ -78,14 +86,14 @@ const fetchUser = (id: string): Task.Result<ApiError, User> =>
|
|
|
78
86
|
if (!r.ok) throw { status: r.status, message: r.statusText };
|
|
79
87
|
return r.json() as Promise<User>;
|
|
80
88
|
}),
|
|
81
|
-
(e) => e as ApiError,
|
|
89
|
+
{ onError: (e) => e as ApiError },
|
|
82
90
|
);
|
|
83
91
|
|
|
84
92
|
const fetchPosts = (userId: string): Task.Result<ApiError, Post[]> =>
|
|
85
93
|
Task.Result.tryCatch(
|
|
86
94
|
(signal) =>
|
|
87
95
|
fetch(`/users/${userId}/posts`, { signal }).then((r) => r.json()),
|
|
88
|
-
(e) => e as ApiError,
|
|
96
|
+
{ onError: (e) => e as ApiError },
|
|
89
97
|
);
|
|
90
98
|
|
|
91
99
|
// Chain two requests — the AbortSignal propagates to both automatically
|
|
@@ -116,7 +124,7 @@ if (Result.is.ok(result)) {
|
|
|
116
124
|
}
|
|
117
125
|
```
|
|
118
126
|
|
|
119
|
-
|
|
127
|
+
### Transforming data collections
|
|
120
128
|
|
|
121
129
|
Standard JavaScript arrays and records routinely return `undefined` on out-of-bounds access or
|
|
122
130
|
missing keys. The utility modules in `pipelined` wrap these operations with data-last, curried
|
|
@@ -156,7 +164,83 @@ replaces a `map` followed by a `filter`. `Arr.head` returns `Maybe<Item>` rather
|
|
|
156
164
|
`Item | undefined`, so the absence is explicit in the type and the rest of the pipeline handles it
|
|
157
165
|
the same way.
|
|
158
166
|
|
|
159
|
-
|
|
167
|
+
### Multi-field form validation
|
|
168
|
+
|
|
169
|
+
In web forms and batch data ingestion, failing fast on the first error produces poor user
|
|
170
|
+
experiences. `Validation<E, A>` accumulates all errors across independent fields simultaneously:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { pipe } from "@nlozgachev/pipelined/composition";
|
|
174
|
+
import { Validation } from "@nlozgachev/pipelined/core";
|
|
175
|
+
import { Str } from "@nlozgachev/pipelined/data";
|
|
176
|
+
|
|
177
|
+
type SignupForm = { username: string; email: string };
|
|
178
|
+
|
|
179
|
+
const validateUsername = (name: string): Validation<string, string> =>
|
|
180
|
+
pipe(
|
|
181
|
+
name,
|
|
182
|
+
Str.trim,
|
|
183
|
+
Validation.from.Predicate(
|
|
184
|
+
(s) => s.length >= 3,
|
|
185
|
+
() => "Username must be at least 3 characters",
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const validateEmail = (email: string): Validation<string, string> =>
|
|
190
|
+
pipe(
|
|
191
|
+
email,
|
|
192
|
+
Str.trim,
|
|
193
|
+
Validation.from.Predicate(
|
|
194
|
+
(s) => s.includes("@"),
|
|
195
|
+
() => "Email must contain an @ symbol",
|
|
196
|
+
),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const validateSignup = (form: SignupForm) =>
|
|
200
|
+
Validation.struct({
|
|
201
|
+
username: validateUsername(form.username),
|
|
202
|
+
email: validateEmail(form.email),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const outcome = validateSignup({ username: "a", email: "invalid" });
|
|
206
|
+
|
|
207
|
+
if (Validation.is.failed(outcome)) {
|
|
208
|
+
console.log(outcome.errors);
|
|
209
|
+
// ["Username must be at least 3 characters", "Email must contain an @ symbol"]
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Eliminating impossible UI states
|
|
214
|
+
|
|
215
|
+
Managing asynchronous data in UI components with separate boolean flags (`isLoading`, `isError`,
|
|
216
|
+
`data`) leads to contradictory combinations (like showing a spinner alongside an error alert).
|
|
217
|
+
`RemoteData` models the complete 4-state lifecycle explicitly:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import { pipe } from "@nlozgachev/pipelined/composition";
|
|
221
|
+
import { RemoteData } from "@nlozgachev/pipelined/core";
|
|
222
|
+
|
|
223
|
+
type UserProfile = { name: string };
|
|
224
|
+
type FetchError = { message: string };
|
|
225
|
+
|
|
226
|
+
const renderUI = (state: RemoteData<FetchError, UserProfile>): string =>
|
|
227
|
+
pipe(
|
|
228
|
+
state,
|
|
229
|
+
RemoteData.match({
|
|
230
|
+
notAsked: () => "Click to load profile",
|
|
231
|
+
loading: () => "Loading...",
|
|
232
|
+
failure: (err) => `Failed: ${err.message}`,
|
|
233
|
+
success: (user) => `Welcome, ${user.name}!`,
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
renderUI(RemoteData.make.notAsked()); // "Click to load profile"
|
|
238
|
+
renderUI(RemoteData.make.loading()); // "Loading..."
|
|
239
|
+
renderUI(RemoteData.make.failure({ message: "Network down" })); // "Failed: Network down"
|
|
240
|
+
renderUI(RemoteData.make.success({ name: "Alice" })); // "Welcome, Alice!"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Managing request lifecycles, retries, and timeouts
|
|
160
244
|
|
|
161
245
|
Handling robust network interactions — including retry attempts, backoff timing, timeouts, and
|
|
162
246
|
signal-driven cancellation — typically requires complex, stateful code that is highly prone to
|
|
@@ -236,13 +320,11 @@ if (Op.isOk(outcome)) {
|
|
|
236
320
|
fetchUser.abort();
|
|
237
321
|
```
|
|
238
322
|
|
|
239
|
-
|
|
323
|
+
### Repeated UI interactions and concurrency strategies
|
|
240
324
|
|
|
241
325
|
User interfaces frequently trigger repeated asynchronous events: a search input firing on every
|
|
242
|
-
keystroke, a submit button clicked multiple times, or
|
|
243
|
-
|
|
244
|
-
state machines. `Op` simplifies this by allowing developers to declare the concurrency strategy as a
|
|
245
|
-
simple configuration choice:
|
|
326
|
+
keystroke, a submit button clicked multiple times, or background draft auto-saving. `Op` lets you
|
|
327
|
+
declare the concurrency strategy as a clean configuration choice:
|
|
246
328
|
|
|
247
329
|
**Search — cancel the previous call when the user types:**
|
|
248
330
|
|
|
@@ -259,7 +341,7 @@ const searchOp = Op.create(
|
|
|
259
341
|
);
|
|
260
342
|
|
|
261
343
|
const search = Op.interpret(searchOp, {
|
|
262
|
-
strategy: "restartable", // new call cancels the previous
|
|
344
|
+
strategy: "restartable", // new call cancels the previous in-flight request
|
|
263
345
|
retry: { attempts: 2, backoff: Duration.milliseconds(300) },
|
|
264
346
|
});
|
|
265
347
|
|
|
@@ -273,7 +355,7 @@ search.subscribe((state) => {
|
|
|
273
355
|
input.addEventListener("input", (e) => search.run(e.currentTarget.value));
|
|
274
356
|
```
|
|
275
357
|
|
|
276
|
-
**Form submit — drop concurrent submissions:**
|
|
358
|
+
**Form submit — drop concurrent duplicate submissions:**
|
|
277
359
|
|
|
278
360
|
```ts
|
|
279
361
|
const submitOp = Op.create(
|
|
@@ -301,14 +383,13 @@ form.addEventListener("submit", (e) => {
|
|
|
301
383
|
```
|
|
302
384
|
|
|
303
385
|
The system supports a variety of built-in strategies — `restartable`, `exclusive`, `debounced`,
|
|
304
|
-
`throttled`, `queue`, `buffered`, `concurrent`, `keyed`, and `once
|
|
305
|
-
complex async scenarios highly predictable.
|
|
386
|
+
`throttled`, `queue`, `buffered`, `concurrent`, `keyed`, and `once`.
|
|
306
387
|
|
|
307
|
-
|
|
388
|
+
### Event streaming, sequence funnels, and queue safety
|
|
308
389
|
|
|
309
390
|
Decoupling event producers from stateful event consumers often leads to untyped event emitters or
|
|
310
|
-
|
|
311
|
-
pattern matching, and
|
|
391
|
+
recursive call stack crashes during event cascades. `Stream` models in-memory event pipelines with
|
|
392
|
+
typed message schemas, multi-step sequence pattern matching, and causal FIFO queue dispatching:
|
|
312
393
|
|
|
313
394
|
```ts
|
|
314
395
|
import { Stream } from "@nlozgachev/pipelined/core";
|
|
@@ -317,15 +398,16 @@ type UserFlowMessages = {
|
|
|
317
398
|
sessionStarted: { sessionId: string };
|
|
318
399
|
stepCompleted: { stepName: string };
|
|
319
400
|
flowFinished: { totalTimeMs: number };
|
|
401
|
+
flowCancelled: { reason: string };
|
|
320
402
|
};
|
|
321
403
|
|
|
322
404
|
const flowStream = Stream.make<UserFlowMessages>();
|
|
323
405
|
|
|
324
|
-
//
|
|
406
|
+
// Pattern-match the complete multi-step funnel
|
|
325
407
|
const sub = Stream.listen(
|
|
326
408
|
flowStream,
|
|
327
409
|
["sessionStarted", "stepCompleted", "flowFinished"],
|
|
328
|
-
{ ordered: true },
|
|
410
|
+
{ ordered: true, reset: "flowCancelled" },
|
|
329
411
|
).reduce(
|
|
330
412
|
(msg, state) => {
|
|
331
413
|
if (msg.kind === "flowFinished") {
|
|
@@ -355,62 +437,135 @@ Stream.emit(flowStream, {
|
|
|
355
437
|
sub.getState(); // { completedFlows: 1 }
|
|
356
438
|
```
|
|
357
439
|
|
|
358
|
-
|
|
440
|
+
`Stream` executes cascading events iteratively using an internal queue with O(1) stack overhead,
|
|
441
|
+
completely preventing stack overflow crashes and out-of-order re-entrant execution.
|
|
359
442
|
|
|
360
|
-
|
|
361
|
-
applications.
|
|
443
|
+
### Deep immutable updates without spread boilerplate
|
|
362
444
|
|
|
363
|
-
|
|
445
|
+
Modifying deeply nested properties in state trees normally requires tedious spread syntax. `Lens`
|
|
446
|
+
and `Optional` turn nested paths into first-class values:
|
|
364
447
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
inclusive-OR scenarios containing a first value, a second, or both simultaneously, and `Tuple`
|
|
369
|
-
provides a strongly-typed, immutable two-element pair.
|
|
448
|
+
```ts
|
|
449
|
+
import { pipe } from "@nlozgachev/pipelined/composition";
|
|
450
|
+
import { Lens } from "@nlozgachev/pipelined/core";
|
|
370
451
|
|
|
371
|
-
|
|
452
|
+
type Address = { city: string; zip: string };
|
|
453
|
+
type User = { name: string; address: Address };
|
|
372
454
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
and signal propagation automatically. `Stream` provides typed event streaming, sequence pattern
|
|
378
|
-
matching, state reduction, and structural forwarding across channels. `Deferred` represents a
|
|
379
|
-
lightweight, infallible asynchronous value that is guaranteed to always resolve without rejection.
|
|
455
|
+
const user: User = {
|
|
456
|
+
name: "Alice",
|
|
457
|
+
address: { city: "Berlin", zip: "10115" },
|
|
458
|
+
};
|
|
380
459
|
|
|
381
|
-
|
|
460
|
+
const addressLens = Lens.from.property<User>()("address");
|
|
461
|
+
const cityLens = Lens.from.property<Address>()("city");
|
|
462
|
+
const userCityLens = pipe(addressLens, Lens.andThen(cityLens));
|
|
382
463
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
464
|
+
// Immutably modify deep property in one line
|
|
465
|
+
const updatedUser = pipe(
|
|
466
|
+
user,
|
|
467
|
+
Lens.modify(userCityLens)((c) => c.toUpperCase()),
|
|
468
|
+
);
|
|
387
469
|
|
|
388
|
-
|
|
470
|
+
updatedUser.address.city; // "BERLIN"
|
|
471
|
+
```
|
|
389
472
|
|
|
390
|
-
|
|
391
|
-
composable object and primitive comparisons, `Ordering` for sorting comparators, and `Combinable`
|
|
392
|
-
for structural monoids (allowing folding collections with a neutral starting point). Composable
|
|
393
|
-
boolean checks and type guards are supported by `Predicate` and `Refinement` abstractions.
|
|
473
|
+
### Nominal type safety and security boundaries
|
|
394
474
|
|
|
395
|
-
|
|
475
|
+
TypeScript's structural typing allows accidental parameter swapping (such as passing an `OrderId`
|
|
476
|
+
into a `UserId` slot). `Brand` creates nominal types with smart constructor validation gates at zero
|
|
477
|
+
runtime cost:
|
|
396
478
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
pipelines.
|
|
479
|
+
```ts
|
|
480
|
+
import { pipe } from "@nlozgachev/pipelined/composition";
|
|
481
|
+
import { Result } from "@nlozgachev/pipelined/core";
|
|
482
|
+
import { Brand } from "@nlozgachev/pipelined/types";
|
|
402
483
|
|
|
403
|
-
|
|
484
|
+
type UserId = Brand<"UserId", string>;
|
|
485
|
+
type OrderId = Brand<"OrderId", string>;
|
|
486
|
+
type Email = Brand<"Email", string>;
|
|
404
487
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
488
|
+
const parseEmail = (raw: string): Result<string, Email> =>
|
|
489
|
+
raw.includes("@")
|
|
490
|
+
? Result.make.ok(raw as Email)
|
|
491
|
+
: Result.make.err("Invalid email address");
|
|
492
|
+
|
|
493
|
+
const sendReceipt = (userId: UserId, orderId: OrderId, email: Email) =>
|
|
494
|
+
`Sent to ${email} for order ${orderId} by user ${userId}`;
|
|
495
|
+
|
|
496
|
+
const user = "usr_100" as UserId;
|
|
497
|
+
const order = "ord_500" as OrderId;
|
|
498
|
+
const email = parseEmail("alice@example.com");
|
|
499
|
+
|
|
500
|
+
if (Result.is.ok(email)) {
|
|
501
|
+
sendReceipt(user, order, email.value);
|
|
502
|
+
// Type checker prevents: sendReceipt(order, user, email.value)
|
|
503
|
+
}
|
|
504
|
+
```
|
|
409
505
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## Quick Reference
|
|
509
|
+
|
|
510
|
+
| Problem to Solve | Module | Import Path |
|
|
511
|
+
| ------------------------------------------------------------- | ----------------------------- | ----------------------------------- |
|
|
512
|
+
| Optional values without `null` / `undefined` checks | `Maybe` | `@nlozgachev/pipelined/core` |
|
|
513
|
+
| Synchronous typed errors without `try`/`catch` | `Result` | `@nlozgachev/pipelined/core` |
|
|
514
|
+
| Multi-field form and batch validation error accumulation | `Validation` | `@nlozgachev/pipelined/core` |
|
|
515
|
+
| Lazy async workflows with automatic `AbortSignal` | `Task.Result`, `Task` | `@nlozgachev/pipelined/core` |
|
|
516
|
+
| Infallible async return container for tasks | `Deferred` | `@nlozgachev/pipelined/core` |
|
|
517
|
+
| Eliminating impossible UI loading/error/data states | `RemoteData` | `@nlozgachev/pipelined/core` |
|
|
518
|
+
| Managing request race conditions, retries, and locks | `Op` | `@nlozgachev/pipelined/core` |
|
|
519
|
+
| In-memory event streaming, sequence funnels & queue safety | `Stream` | `@nlozgachev/pipelined/core` |
|
|
520
|
+
| Deep nested immutable updates without spread boilerplate | `Lens`, `Optional` | `@nlozgachev/pipelined/core` |
|
|
521
|
+
| Implicit dependency injection without prop drilling | `Reader` | `@nlozgachev/pipelined/core` |
|
|
522
|
+
| Pure state transitions, tokenizers, and parsers | `State` | `@nlozgachev/pipelined/core` |
|
|
523
|
+
| Deterministic bracket cleanup (DB pools, file locks) | `Resource` | `@nlozgachev/pipelined/core` |
|
|
524
|
+
| Deferred computation memoized on first access | `Lazy` | `@nlozgachev/pipelined/core` |
|
|
525
|
+
| Pure calculation audit trails and decision logging | `Logged` | `@nlozgachev/pipelined/core` |
|
|
526
|
+
| Inclusive-OR data modeling and two-way sync diffs | `These` | `@nlozgachev/pipelined/core` |
|
|
527
|
+
| Deep structural equality & React component memoization | `Equality` | `@nlozgachev/pipelined/core` |
|
|
528
|
+
| Multi-column table sorting with tiebreakers | `Ordering` | `@nlozgachev/pipelined/core` |
|
|
529
|
+
| Composable boolean filter pipelines & authorization policies | `Predicate` | `@nlozgachev/pipelined/core` |
|
|
530
|
+
| Runtime type narrowing & custom type guard composition | `Refinement` | `@nlozgachev/pipelined/core` |
|
|
531
|
+
| Merging configurations & metric structures (Monoids) | `Combinable` | `@nlozgachev/pipelined/core` |
|
|
532
|
+
| Strongly-typed immutable pair manipulation | `Tuple` | `@nlozgachev/pipelined/core` |
|
|
533
|
+
| Point-free, bounds-safe array transformations | `Arr`, `Arr.NonEmpty` | `@nlozgachev/pipelined/data` |
|
|
534
|
+
| Type-safe object manipulation & key migration | `Rec`, `Rec.NonEmpty` | `@nlozgachev/pipelined/data` |
|
|
535
|
+
| Insertion-ordered maps with non-string keys | `Dict`, `Dict.NonEmpty` | `@nlozgachev/pipelined/data` |
|
|
536
|
+
| Immutable sets & role/permission algebra | `Uniq` | `@nlozgachev/pipelined/data` |
|
|
537
|
+
| String sanitization, numeric conversion & slug parsing | `Str` | `@nlozgachev/pipelined/data` |
|
|
538
|
+
| Boundary clamping & division-by-zero protection | `Num` | `@nlozgachev/pipelined/data` |
|
|
539
|
+
| Financial ledger arithmetic without float precision drift | `BigNum` | `@nlozgachev/pipelined/data` |
|
|
540
|
+
| Safe JSON parsing & circular reference protection | `Json` | `@nlozgachev/pipelined/data` |
|
|
541
|
+
| Nominal typing & security boundary gates | `Brand` | `@nlozgachev/pipelined/types` |
|
|
542
|
+
| Explicit, unit-safe time spans & timeout policies | `Duration`, `RetryPolicy` | `@nlozgachev/pipelined/types` |
|
|
543
|
+
| Left-to-right value pipeline execution | `pipe` | `@nlozgachev/pipelined/composition` |
|
|
544
|
+
| Left-to-right and right-to-left function composition | `flow`, `compose` | `@nlozgachev/pipelined/composition` |
|
|
545
|
+
| Currying, uncurrying, and argument flipping | `curry`, `uncurry`, `flip` | `@nlozgachev/pipelined/composition` |
|
|
546
|
+
| Multi-branch argument routing and combining | `converge`, `juxt`, `on` | `@nlozgachev/pipelined/composition` |
|
|
547
|
+
| Pure function memoization, predicates & pipeline side-effects | `memoize`, `tap`, `not`, `fn` | `@nlozgachev/pipelined/composition` |
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## Package Architecture and Performance
|
|
552
|
+
|
|
553
|
+
`pipelined` is structured into 4 isolated, tree-shakeable entry points:
|
|
554
|
+
|
|
555
|
+
- **`@nlozgachev/pipelined/core`**: Core context containers, async runtimes, optics, and logic
|
|
556
|
+
abstractions (<16 KB gzipped).
|
|
557
|
+
- **`@nlozgachev/pipelined/data`**: Curried, data-last utilities for collections, numbers, strings,
|
|
558
|
+
and JSON (<13 KB gzipped).
|
|
559
|
+
- **`@nlozgachev/pipelined/composition`**: Pure higher-order function combinators (`pipe`, `flow`,
|
|
560
|
+
`compose`, `curry`, `uncurry`, `converge`, `juxt`, `memoize`, `tap`, `on`, `not`, `flip`, `fn`)
|
|
561
|
+
(<4 KB gzipped).
|
|
562
|
+
- **`@nlozgachev/pipelined/types`**: Type-level utilities (`Brand`, `Duration`, `RetryPolicy`) (<2
|
|
563
|
+
KB gzipped).
|
|
564
|
+
|
|
565
|
+
Every utility in the library is benchmarked against its native equivalent. While currying introduces
|
|
566
|
+
a small function call overhead for composability, the library uses custom algorithms for
|
|
567
|
+
data-structure methods whenever the native JavaScript implementations are slower, ensuring the
|
|
568
|
+
fastest execution path possible.
|
|
414
569
|
|
|
415
570
|
## License
|
|
416
571
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nlozgachev/pipelined",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"description": "Opinionated functional abstractions for TypeScript",
|
|
5
5
|
"license": "BSD-3-Clause",
|
|
6
6
|
"homepage": "https://pipelined.lozgachev.dev",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "tsup",
|
|
57
57
|
"test": "vitest run",
|
|
58
|
+
"test:coverage": "vitest run --coverage",
|
|
58
59
|
"bench": "vitest bench",
|
|
59
60
|
"size": "size-limit",
|
|
60
61
|
"typecheck": "tsc --noEmit",
|