@nlozgachev/pipelined 0.49.0 → 0.51.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 +222 -67
- package/dist/{chunk-EPT3GYGN.mjs → chunk-6N27QZFK.mjs} +1 -1
- package/dist/{chunk-IHLH2XYQ.mjs → chunk-U64ASY7P.mjs} +34 -10
- package/dist/core.d.mts +8 -0
- package/dist/core.d.ts +8 -0
- package/dist/core.js +34 -10
- package/dist/core.mjs +1 -1
- package/dist/data.mjs +2 -2
- package/dist/index.js +34 -10
- package/dist/index.mjs +2 -2
- package/package.json +37 -16
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.
|
|
442
|
+
|
|
443
|
+
### Deep immutable updates without spread boilerplate
|
|
359
444
|
|
|
360
|
-
|
|
361
|
-
|
|
445
|
+
Modifying deeply nested properties in state trees normally requires tedious spread syntax. `Lens`
|
|
446
|
+
and `Optional` turn nested paths into first-class values:
|
|
362
447
|
|
|
363
|
-
|
|
448
|
+
```ts
|
|
449
|
+
import { pipe } from "@nlozgachev/pipelined/composition";
|
|
450
|
+
import { Lens } from "@nlozgachev/pipelined/core";
|
|
451
|
+
|
|
452
|
+
type Address = { city: string; zip: string };
|
|
453
|
+
type User = { name: string; address: Address };
|
|
454
|
+
|
|
455
|
+
const user: User = {
|
|
456
|
+
name: "Alice",
|
|
457
|
+
address: { city: "Berlin", zip: "10115" },
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const addressLens = Lens.from.property<User>()("address");
|
|
461
|
+
const cityLens = Lens.from.property<Address>()("city");
|
|
462
|
+
const userCityLens = pipe(addressLens, Lens.andThen(cityLens));
|
|
364
463
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
464
|
+
// Immutably modify deep property in one line
|
|
465
|
+
const updatedUser = pipe(
|
|
466
|
+
user,
|
|
467
|
+
Lens.modify(userCityLens)((c) => c.toUpperCase()),
|
|
468
|
+
);
|
|
370
469
|
|
|
371
|
-
|
|
470
|
+
updatedUser.address.city; // "BERLIN"
|
|
471
|
+
```
|
|
372
472
|
|
|
373
|
-
|
|
374
|
-
handled by `Task.Result`, `Task.Maybe`, and `Task.Validation`. For managing stateful, recurring
|
|
375
|
-
asynchronous operations with complex scheduling, `Op` implements named concurrency strategies such
|
|
376
|
-
as `restartable`, `exclusive`, `debounced`, `throttled`, and `queue`, handling retries, timeouts,
|
|
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.
|
|
473
|
+
### Nominal type safety and security boundaries
|
|
380
474
|
|
|
381
|
-
|
|
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:
|
|
382
478
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
479
|
+
```ts
|
|
480
|
+
import { pipe } from "@nlozgachev/pipelined/composition";
|
|
481
|
+
import { Result } from "@nlozgachev/pipelined/core";
|
|
482
|
+
import { Brand } from "@nlozgachev/pipelined/types";
|
|
387
483
|
|
|
388
|
-
|
|
484
|
+
type UserId = Brand<"UserId", string>;
|
|
485
|
+
type OrderId = Brand<"OrderId", string>;
|
|
486
|
+
type Email = Brand<"Email", string>;
|
|
389
487
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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");
|
|
394
492
|
|
|
395
|
-
|
|
493
|
+
const sendReceipt = (userId: UserId, orderId: OrderId, email: Email) =>
|
|
494
|
+
`Sent to ${email} for order ${orderId} by user ${userId}`;
|
|
396
495
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
helpers like `when`, `unless`, `either`, `safe`, and `async` to support robust, expressive
|
|
401
|
-
pipelines.
|
|
496
|
+
const user = "usr_100" as UserId;
|
|
497
|
+
const order = "ord_500" as OrderId;
|
|
498
|
+
const email = parseEmail("alice@example.com");
|
|
402
499
|
|
|
403
|
-
|
|
500
|
+
if (Result.is.ok(email)) {
|
|
501
|
+
sendReceipt(user, order, email.value);
|
|
502
|
+
// Type checker prevents: sendReceipt(order, user, email.value)
|
|
503
|
+
}
|
|
504
|
+
```
|
|
404
505
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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).
|
|
409
564
|
|
|
410
565
|
Every utility in the library is benchmarked against its native equivalent. The data-last currying
|
|
411
|
-
adds a
|
|
412
|
-
where native overhead is significant, custom implementations are used that often run
|
|
413
|
-
their native counterparts.
|
|
566
|
+
adds a negligible function call overhead, which is the expected cost of composability. For
|
|
567
|
+
operations where native overhead is significant, custom implementations are used that often run
|
|
568
|
+
faster than their native counterparts.
|
|
414
569
|
|
|
415
570
|
## License
|
|
416
571
|
|
|
@@ -1737,22 +1737,35 @@ var Stream;
|
|
|
1737
1737
|
((Stream2) => {
|
|
1738
1738
|
Stream2.make = (options) => ({
|
|
1739
1739
|
options,
|
|
1740
|
-
_listeners: /* @__PURE__ */ new Set()
|
|
1740
|
+
_listeners: /* @__PURE__ */ new Set(),
|
|
1741
|
+
_queue: [],
|
|
1742
|
+
_isEmitting: false
|
|
1741
1743
|
});
|
|
1742
1744
|
Stream2.emit = (target, message) => {
|
|
1743
1745
|
const targets = Array.isArray(target) ? target : [target];
|
|
1744
1746
|
const msg = message;
|
|
1745
1747
|
for (const stream of targets) {
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
+
stream._queue.push(msg);
|
|
1749
|
+
if (!stream._isEmitting) {
|
|
1750
|
+
stream._isEmitting = true;
|
|
1748
1751
|
try {
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1752
|
+
while (stream._queue.length > 0) {
|
|
1753
|
+
const nextMsg = stream._queue.shift();
|
|
1754
|
+
const listeners = Array.from(stream._listeners);
|
|
1755
|
+
for (const listener of listeners) {
|
|
1756
|
+
try {
|
|
1757
|
+
listener(nextMsg);
|
|
1758
|
+
} catch (err2) {
|
|
1759
|
+
if (stream.options?.onError) {
|
|
1760
|
+
stream.options.onError(err2);
|
|
1761
|
+
} else {
|
|
1762
|
+
throw err2;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1755
1766
|
}
|
|
1767
|
+
} finally {
|
|
1768
|
+
stream._isEmitting = false;
|
|
1756
1769
|
}
|
|
1757
1770
|
}
|
|
1758
1771
|
}
|
|
@@ -1779,6 +1792,7 @@ var Stream;
|
|
|
1779
1792
|
const isStrict = options?.strict ?? false;
|
|
1780
1793
|
const isOnce = options?.once ?? false;
|
|
1781
1794
|
const resetKinds = options?.reset ? new Set(Array.isArray(options.reset) ? options.reset : [options.reset]) : null;
|
|
1795
|
+
const optionalKinds = options?.optional ? new Set(Array.isArray(options.optional) ? options.optional : [options.optional]) : null;
|
|
1782
1796
|
const createMatcher = (onMatch) => {
|
|
1783
1797
|
let sequenceIndex = 0;
|
|
1784
1798
|
return (msg) => {
|
|
@@ -1792,7 +1806,17 @@ var Stream;
|
|
|
1792
1806
|
}
|
|
1793
1807
|
return;
|
|
1794
1808
|
}
|
|
1795
|
-
|
|
1809
|
+
let expectedKind = eventList[sequenceIndex];
|
|
1810
|
+
if (expectedKind !== msg.kind && optionalKinds !== null) {
|
|
1811
|
+
let lookaheadIndex = sequenceIndex;
|
|
1812
|
+
while (lookaheadIndex < eventList.length && optionalKinds.has(eventList[lookaheadIndex]) && eventList[lookaheadIndex] !== msg.kind) {
|
|
1813
|
+
lookaheadIndex++;
|
|
1814
|
+
}
|
|
1815
|
+
if (lookaheadIndex < eventList.length && eventList[lookaheadIndex] === msg.kind) {
|
|
1816
|
+
sequenceIndex = lookaheadIndex;
|
|
1817
|
+
expectedKind = eventList[sequenceIndex];
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1796
1820
|
if (msg.kind === expectedKind) {
|
|
1797
1821
|
sequenceIndex++;
|
|
1798
1822
|
if (sequenceIndex === eventList.length) {
|
package/dist/core.d.mts
CHANGED
|
@@ -2490,6 +2490,10 @@ type Stream<S extends Record<string, unknown>> = {
|
|
|
2490
2490
|
readonly options?: Stream.Options;
|
|
2491
2491
|
/** @internal */
|
|
2492
2492
|
readonly _listeners: Set<(msg: Stream.Message<S>) => void>;
|
|
2493
|
+
/** @internal */
|
|
2494
|
+
readonly _queue: Array<Stream.Message<S>>;
|
|
2495
|
+
/** @internal */
|
|
2496
|
+
_isEmitting: boolean;
|
|
2493
2497
|
};
|
|
2494
2498
|
declare namespace Stream {
|
|
2495
2499
|
/**
|
|
@@ -2517,6 +2521,8 @@ declare namespace Stream {
|
|
|
2517
2521
|
readonly once?: boolean;
|
|
2518
2522
|
/** Event kind(s) that reset sequence tracking to index 0. */
|
|
2519
2523
|
readonly reset?: (keyof S & string) | ReadonlyArray<keyof S & string>;
|
|
2524
|
+
/** Event kind(s) in the sequence that may be present or skipped. */
|
|
2525
|
+
readonly optional?: (keyof S & string) | ReadonlyArray<keyof S & string>;
|
|
2520
2526
|
};
|
|
2521
2527
|
/**
|
|
2522
2528
|
* Handle for an active stateful subscription.
|
|
@@ -2558,6 +2564,8 @@ declare namespace Stream {
|
|
|
2558
2564
|
/**
|
|
2559
2565
|
* Emits a message payload to one or more target streams.
|
|
2560
2566
|
*
|
|
2567
|
+
* Uses a synchronous breadth-first trampoline queue to handle re-entrant emissions deterministically.
|
|
2568
|
+
*
|
|
2561
2569
|
* @example
|
|
2562
2570
|
* ```ts
|
|
2563
2571
|
* Stream.emit(streamA, {
|
package/dist/core.d.ts
CHANGED
|
@@ -2490,6 +2490,10 @@ type Stream<S extends Record<string, unknown>> = {
|
|
|
2490
2490
|
readonly options?: Stream.Options;
|
|
2491
2491
|
/** @internal */
|
|
2492
2492
|
readonly _listeners: Set<(msg: Stream.Message<S>) => void>;
|
|
2493
|
+
/** @internal */
|
|
2494
|
+
readonly _queue: Array<Stream.Message<S>>;
|
|
2495
|
+
/** @internal */
|
|
2496
|
+
_isEmitting: boolean;
|
|
2493
2497
|
};
|
|
2494
2498
|
declare namespace Stream {
|
|
2495
2499
|
/**
|
|
@@ -2517,6 +2521,8 @@ declare namespace Stream {
|
|
|
2517
2521
|
readonly once?: boolean;
|
|
2518
2522
|
/** Event kind(s) that reset sequence tracking to index 0. */
|
|
2519
2523
|
readonly reset?: (keyof S & string) | ReadonlyArray<keyof S & string>;
|
|
2524
|
+
/** Event kind(s) in the sequence that may be present or skipped. */
|
|
2525
|
+
readonly optional?: (keyof S & string) | ReadonlyArray<keyof S & string>;
|
|
2520
2526
|
};
|
|
2521
2527
|
/**
|
|
2522
2528
|
* Handle for an active stateful subscription.
|
|
@@ -2558,6 +2564,8 @@ declare namespace Stream {
|
|
|
2558
2564
|
/**
|
|
2559
2565
|
* Emits a message payload to one or more target streams.
|
|
2560
2566
|
*
|
|
2567
|
+
* Uses a synchronous breadth-first trampoline queue to handle re-entrant emissions deterministically.
|
|
2568
|
+
*
|
|
2561
2569
|
* @example
|
|
2562
2570
|
* ```ts
|
|
2563
2571
|
* Stream.emit(streamA, {
|
package/dist/core.js
CHANGED
|
@@ -1811,22 +1811,35 @@ var Stream;
|
|
|
1811
1811
|
((Stream2) => {
|
|
1812
1812
|
Stream2.make = (options) => ({
|
|
1813
1813
|
options,
|
|
1814
|
-
_listeners: /* @__PURE__ */ new Set()
|
|
1814
|
+
_listeners: /* @__PURE__ */ new Set(),
|
|
1815
|
+
_queue: [],
|
|
1816
|
+
_isEmitting: false
|
|
1815
1817
|
});
|
|
1816
1818
|
Stream2.emit = (target, message) => {
|
|
1817
1819
|
const targets = Array.isArray(target) ? target : [target];
|
|
1818
1820
|
const msg = message;
|
|
1819
1821
|
for (const stream of targets) {
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
+
stream._queue.push(msg);
|
|
1823
|
+
if (!stream._isEmitting) {
|
|
1824
|
+
stream._isEmitting = true;
|
|
1822
1825
|
try {
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1826
|
+
while (stream._queue.length > 0) {
|
|
1827
|
+
const nextMsg = stream._queue.shift();
|
|
1828
|
+
const listeners = Array.from(stream._listeners);
|
|
1829
|
+
for (const listener of listeners) {
|
|
1830
|
+
try {
|
|
1831
|
+
listener(nextMsg);
|
|
1832
|
+
} catch (err2) {
|
|
1833
|
+
if (stream.options?.onError) {
|
|
1834
|
+
stream.options.onError(err2);
|
|
1835
|
+
} else {
|
|
1836
|
+
throw err2;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1829
1840
|
}
|
|
1841
|
+
} finally {
|
|
1842
|
+
stream._isEmitting = false;
|
|
1830
1843
|
}
|
|
1831
1844
|
}
|
|
1832
1845
|
}
|
|
@@ -1853,6 +1866,7 @@ var Stream;
|
|
|
1853
1866
|
const isStrict = options?.strict ?? false;
|
|
1854
1867
|
const isOnce = options?.once ?? false;
|
|
1855
1868
|
const resetKinds = options?.reset ? new Set(Array.isArray(options.reset) ? options.reset : [options.reset]) : null;
|
|
1869
|
+
const optionalKinds = options?.optional ? new Set(Array.isArray(options.optional) ? options.optional : [options.optional]) : null;
|
|
1856
1870
|
const createMatcher = (onMatch) => {
|
|
1857
1871
|
let sequenceIndex = 0;
|
|
1858
1872
|
return (msg) => {
|
|
@@ -1866,7 +1880,17 @@ var Stream;
|
|
|
1866
1880
|
}
|
|
1867
1881
|
return;
|
|
1868
1882
|
}
|
|
1869
|
-
|
|
1883
|
+
let expectedKind = eventList[sequenceIndex];
|
|
1884
|
+
if (expectedKind !== msg.kind && optionalKinds !== null) {
|
|
1885
|
+
let lookaheadIndex = sequenceIndex;
|
|
1886
|
+
while (lookaheadIndex < eventList.length && optionalKinds.has(eventList[lookaheadIndex]) && eventList[lookaheadIndex] !== msg.kind) {
|
|
1887
|
+
lookaheadIndex++;
|
|
1888
|
+
}
|
|
1889
|
+
if (lookaheadIndex < eventList.length && eventList[lookaheadIndex] === msg.kind) {
|
|
1890
|
+
sequenceIndex = lookaheadIndex;
|
|
1891
|
+
expectedKind = eventList[sequenceIndex];
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1870
1894
|
if (msg.kind === expectedKind) {
|
|
1871
1895
|
sequenceIndex++;
|
|
1872
1896
|
if (sequenceIndex === eventList.length) {
|
package/dist/core.mjs
CHANGED
package/dist/data.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -2293,22 +2293,35 @@ var Stream;
|
|
|
2293
2293
|
((Stream2) => {
|
|
2294
2294
|
Stream2.make = (options) => ({
|
|
2295
2295
|
options,
|
|
2296
|
-
_listeners: /* @__PURE__ */ new Set()
|
|
2296
|
+
_listeners: /* @__PURE__ */ new Set(),
|
|
2297
|
+
_queue: [],
|
|
2298
|
+
_isEmitting: false
|
|
2297
2299
|
});
|
|
2298
2300
|
Stream2.emit = (target, message) => {
|
|
2299
2301
|
const targets = Array.isArray(target) ? target : [target];
|
|
2300
2302
|
const msg = message;
|
|
2301
2303
|
for (const stream of targets) {
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
+
stream._queue.push(msg);
|
|
2305
|
+
if (!stream._isEmitting) {
|
|
2306
|
+
stream._isEmitting = true;
|
|
2304
2307
|
try {
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2308
|
+
while (stream._queue.length > 0) {
|
|
2309
|
+
const nextMsg = stream._queue.shift();
|
|
2310
|
+
const listeners = Array.from(stream._listeners);
|
|
2311
|
+
for (const listener of listeners) {
|
|
2312
|
+
try {
|
|
2313
|
+
listener(nextMsg);
|
|
2314
|
+
} catch (err2) {
|
|
2315
|
+
if (stream.options?.onError) {
|
|
2316
|
+
stream.options.onError(err2);
|
|
2317
|
+
} else {
|
|
2318
|
+
throw err2;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2311
2322
|
}
|
|
2323
|
+
} finally {
|
|
2324
|
+
stream._isEmitting = false;
|
|
2312
2325
|
}
|
|
2313
2326
|
}
|
|
2314
2327
|
}
|
|
@@ -2335,6 +2348,7 @@ var Stream;
|
|
|
2335
2348
|
const isStrict = options?.strict ?? false;
|
|
2336
2349
|
const isOnce = options?.once ?? false;
|
|
2337
2350
|
const resetKinds = options?.reset ? new Set(Array.isArray(options.reset) ? options.reset : [options.reset]) : null;
|
|
2351
|
+
const optionalKinds = options?.optional ? new Set(Array.isArray(options.optional) ? options.optional : [options.optional]) : null;
|
|
2338
2352
|
const createMatcher = (onMatch) => {
|
|
2339
2353
|
let sequenceIndex = 0;
|
|
2340
2354
|
return (msg) => {
|
|
@@ -2348,7 +2362,17 @@ var Stream;
|
|
|
2348
2362
|
}
|
|
2349
2363
|
return;
|
|
2350
2364
|
}
|
|
2351
|
-
|
|
2365
|
+
let expectedKind = eventList[sequenceIndex];
|
|
2366
|
+
if (expectedKind !== msg.kind && optionalKinds !== null) {
|
|
2367
|
+
let lookaheadIndex = sequenceIndex;
|
|
2368
|
+
while (lookaheadIndex < eventList.length && optionalKinds.has(eventList[lookaheadIndex]) && eventList[lookaheadIndex] !== msg.kind) {
|
|
2369
|
+
lookaheadIndex++;
|
|
2370
|
+
}
|
|
2371
|
+
if (lookaheadIndex < eventList.length && eventList[lookaheadIndex] === msg.kind) {
|
|
2372
|
+
sequenceIndex = lookaheadIndex;
|
|
2373
|
+
expectedKind = eventList[sequenceIndex];
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2352
2376
|
if (msg.kind === expectedKind) {
|
|
2353
2377
|
sequenceIndex++;
|
|
2354
2378
|
if (sequenceIndex === eventList.length) {
|
package/dist/index.mjs
CHANGED
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
Rec,
|
|
40
40
|
Str,
|
|
41
41
|
Uniq
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-6N27QZFK.mjs";
|
|
43
43
|
import {
|
|
44
44
|
Combinable,
|
|
45
45
|
Deferred,
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
These,
|
|
67
67
|
Tuple,
|
|
68
68
|
Validation
|
|
69
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-U64ASY7P.mjs";
|
|
70
70
|
import {
|
|
71
71
|
Brand,
|
|
72
72
|
Duration,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nlozgachev/pipelined",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.0",
|
|
4
4
|
"description": "Opinionated functional abstractions for TypeScript",
|
|
5
5
|
"license": "BSD-3-Clause",
|
|
6
6
|
"homepage": "https://pipelined.lozgachev.dev",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
19
|
"sideEffects": false,
|
|
20
20
|
"engines": {
|
|
21
|
-
"node": ">=24"
|
|
21
|
+
"node": ">=24.19.0"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"dist"
|
|
@@ -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",
|
|
@@ -68,24 +69,44 @@
|
|
|
68
69
|
"docs:build": "pnpm --filter pipelined-docs build"
|
|
69
70
|
},
|
|
70
71
|
"devDependencies": {
|
|
71
|
-
"@size-limit/file": "
|
|
72
|
-
"@types/node": "
|
|
73
|
-
"@vitest/coverage-v8": "4.1.
|
|
74
|
-
"bumpp": "
|
|
75
|
-
"dprint": "0.
|
|
76
|
-
"fast-check": "4.
|
|
77
|
-
"oxlint": "1.
|
|
78
|
-
"size-limit": "
|
|
72
|
+
"@size-limit/file": "13.0.3",
|
|
73
|
+
"@types/node": "26.2.0",
|
|
74
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
75
|
+
"bumpp": "12.2.0",
|
|
76
|
+
"dprint": "0.55.2",
|
|
77
|
+
"fast-check": "4.9.0",
|
|
78
|
+
"oxlint": "1.78.0",
|
|
79
|
+
"size-limit": "13.0.3",
|
|
79
80
|
"tsup": "8.5.1",
|
|
80
81
|
"typescript": "6.0.3",
|
|
81
|
-
"vitest": "4.1.
|
|
82
|
+
"vitest": "4.1.10"
|
|
82
83
|
},
|
|
83
84
|
"size-limit": [
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
{
|
|
86
|
+
"path": "dist/index.js",
|
|
87
|
+
"limit": "25 KB",
|
|
88
|
+
"gzip": true
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"path": "dist/core.js",
|
|
92
|
+
"limit": "16 KB",
|
|
93
|
+
"gzip": true
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"path": "dist/composition.js",
|
|
97
|
+
"limit": "4 KB",
|
|
98
|
+
"gzip": true
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"path": "dist/data.js",
|
|
102
|
+
"limit": "13 KB",
|
|
103
|
+
"gzip": true
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"path": "dist/types.js",
|
|
107
|
+
"limit": "2 KB",
|
|
108
|
+
"gzip": true
|
|
109
|
+
}
|
|
89
110
|
],
|
|
90
111
|
"packageManager": "pnpm@11.6.0+sha512.9a36518224080c6fe5165afdcfe79bfa118c29be703f3f462b1e32efe1e98e47e8750b148e08286250aad4113cc7993ca413c4e2cd447752708c2ee5751bc95f"
|
|
91
112
|
}
|