@nlozgachev/pipelined 0.48.0 → 0.50.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
@@ -18,11 +18,12 @@ In mainstream TypeScript, code is often burdened by implicit control flow: unche
18
18
  manual null propagation, and unhandled asynchronous failures. `pipelined` turns these complex
19
19
  runtime states into simple, transparent data structures that compose. By representing optionality as
20
20
  `Maybe`, failures as `Result`, lazy asynchronous pipelines as `Task.Result`, and repeated stateful
21
- interactions as `Op`, the library helps disentangle business logic from control mechanics.
21
+ interactions as `Op` and `Stream`, the library helps disentangle business logic from control
22
+ mechanics.
22
23
 
23
24
  To support these patterns without introducing bloat, the library is designed to be lightweight,
24
- zero-dependency, and fully tree-shakeable. The core module (`/core`) is under 14 KB gzipped, and the
25
- entire toolkit is under 21 KB gzipped, making it equally suitable for client and server
25
+ zero-dependency, and fully tree-shakeable. The core module (`/core`) is under 16 KB gzipped, and the
26
+ entire toolkit is under 25 KB gzipped, making it equally suitable for client and server
26
27
  environments.
27
28
 
28
29
  ## Documentation
@@ -303,6 +304,57 @@ The system supports a variety of built-in strategies — `restartable`, `exclusi
303
304
  `throttled`, `queue`, `buffered`, `concurrent`, `keyed`, and `once` — making the integration of
304
305
  complex async scenarios highly predictable.
305
306
 
307
+ ## Example: typed event streaming and sequence reduction
308
+
309
+ Decoupling event producers from stateful event consumers often leads to untyped event emitters or
310
+ complex ad-hoc state machines. `Stream` models event pipelines with typed message schemas, sequence
311
+ pattern matching, and state reduction:
312
+
313
+ ```ts
314
+ import { Stream } from "@nlozgachev/pipelined/core";
315
+
316
+ type UserFlowMessages = {
317
+ sessionStarted: { sessionId: string };
318
+ stepCompleted: { stepName: string };
319
+ flowFinished: { totalTimeMs: number };
320
+ };
321
+
322
+ const flowStream = Stream.make<UserFlowMessages>();
323
+
324
+ // Match sequence: sessionStarted -> stepCompleted -> flowFinished
325
+ const sub = Stream.listen(
326
+ flowStream,
327
+ ["sessionStarted", "stepCompleted", "flowFinished"],
328
+ { ordered: true },
329
+ ).reduce(
330
+ (msg, state) => {
331
+ if (msg.kind === "flowFinished") {
332
+ return { completedFlows: state.completedFlows + 1 };
333
+ }
334
+ return state;
335
+ },
336
+ { completedFlows: 0 },
337
+ );
338
+
339
+ // Emit typed messages to the stream
340
+ Stream.emit(flowStream, {
341
+ kind: "sessionStarted",
342
+ value: { sessionId: "sess-101" },
343
+ });
344
+
345
+ Stream.emit(flowStream, {
346
+ kind: "stepCompleted",
347
+ value: { stepName: "onboarding" },
348
+ });
349
+
350
+ Stream.emit(flowStream, {
351
+ kind: "flowFinished",
352
+ value: { totalTimeMs: 4200 },
353
+ });
354
+
355
+ sub.getState(); // { completedFlows: 1 }
356
+ ```
357
+
306
358
  ## What is included
307
359
 
308
360
  The library covers the full spectrum of state and control flow scenarios encountered in production
@@ -322,8 +374,9 @@ provides a strongly-typed, immutable two-element pair.
322
374
  handled by `Task.Result`, `Task.Maybe`, and `Task.Validation`. For managing stateful, recurring
323
375
  asynchronous operations with complex scheduling, `Op` implements named concurrency strategies such
324
376
  as `restartable`, `exclusive`, `debounced`, `throttled`, and `queue`, handling retries, timeouts,
325
- and signal propagation automatically. `Deferred` represents a lightweight, infallible asynchronous
326
- value that is guaranteed to always resolve without rejection.
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.
327
380
 
328
381
  ### Optics and environment state
329
382
 
@@ -4,7 +4,7 @@ import {
4
4
  Result,
5
5
  Task,
6
6
  isNonEmptyArr
7
- } from "./chunk-LR63GW6J.mjs";
7
+ } from "./chunk-U64ASY7P.mjs";
8
8
 
9
9
  // src/Data/Arr.ts
10
10
  var ArrMaybe;
@@ -1732,6 +1732,136 @@ var State;
1732
1732
  };
1733
1733
  })(State || (State = {}));
1734
1734
 
1735
+ // src/Core/Stream.ts
1736
+ var Stream;
1737
+ ((Stream2) => {
1738
+ Stream2.make = (options) => ({
1739
+ options,
1740
+ _listeners: /* @__PURE__ */ new Set(),
1741
+ _queue: [],
1742
+ _isEmitting: false
1743
+ });
1744
+ Stream2.emit = (target, message) => {
1745
+ const targets = Array.isArray(target) ? target : [target];
1746
+ const msg = message;
1747
+ for (const stream of targets) {
1748
+ stream._queue.push(msg);
1749
+ if (!stream._isEmitting) {
1750
+ stream._isEmitting = true;
1751
+ try {
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
+ }
1766
+ }
1767
+ } finally {
1768
+ stream._isEmitting = false;
1769
+ }
1770
+ }
1771
+ }
1772
+ };
1773
+ Stream2.forward = (options) => {
1774
+ const targets = Array.isArray(options.to) ? options.to : [options.to];
1775
+ const filterSet = options.only ? new Set(options.only) : null;
1776
+ const handler = (msg) => {
1777
+ if (filterSet !== null && !filterSet.has(msg.kind)) {
1778
+ return;
1779
+ }
1780
+ for (const target of targets) {
1781
+ (0, Stream2.emit)(target, msg);
1782
+ }
1783
+ };
1784
+ options.from._listeners.add(handler);
1785
+ return () => {
1786
+ options.from._listeners.delete(handler);
1787
+ };
1788
+ };
1789
+ Stream2.listen = (stream, events, options) => {
1790
+ const eventList = Array.isArray(events) ? events : [events];
1791
+ const isOrdered = options?.ordered ?? false;
1792
+ const isStrict = options?.strict ?? false;
1793
+ const isOnce = options?.once ?? false;
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;
1796
+ const createMatcher = (onMatch) => {
1797
+ let sequenceIndex = 0;
1798
+ return (msg) => {
1799
+ if (resetKinds !== null && resetKinds.has(msg.kind)) {
1800
+ sequenceIndex = 0;
1801
+ return;
1802
+ }
1803
+ if (!isOrdered) {
1804
+ if (eventList.includes(msg.kind)) {
1805
+ onMatch(msg);
1806
+ }
1807
+ return;
1808
+ }
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
+ }
1820
+ if (msg.kind === expectedKind) {
1821
+ sequenceIndex++;
1822
+ if (sequenceIndex === eventList.length) {
1823
+ sequenceIndex = 0;
1824
+ onMatch(msg);
1825
+ }
1826
+ } else if (isStrict) {
1827
+ sequenceIndex = msg.kind === eventList[0] ? 1 : 0;
1828
+ } else if (eventList.includes(msg.kind)) {
1829
+ sequenceIndex = msg.kind === eventList[0] ? 1 : 0;
1830
+ }
1831
+ };
1832
+ };
1833
+ return {
1834
+ reduce: (reducer, initialState) => {
1835
+ let currentState = initialState;
1836
+ const listenerFn = createMatcher((msg) => {
1837
+ currentState = reducer(msg, currentState);
1838
+ if (isOnce) {
1839
+ stream._listeners.delete(listenerFn);
1840
+ }
1841
+ });
1842
+ const unsubscribe = () => {
1843
+ stream._listeners.delete(listenerFn);
1844
+ };
1845
+ stream._listeners.add(listenerFn);
1846
+ return { unsubscribe, getState: () => currentState };
1847
+ },
1848
+ tap: (effect) => {
1849
+ const listenerFn = createMatcher((msg) => {
1850
+ effect(msg);
1851
+ if (isOnce) {
1852
+ stream._listeners.delete(listenerFn);
1853
+ }
1854
+ });
1855
+ const unsubscribe = () => {
1856
+ stream._listeners.delete(listenerFn);
1857
+ };
1858
+ stream._listeners.add(listenerFn);
1859
+ return unsubscribe;
1860
+ }
1861
+ };
1862
+ };
1863
+ })(Stream || (Stream = {}));
1864
+
1735
1865
  // src/Core/TaskMaybe.ts
1736
1866
  var TaskMaybe;
1737
1867
  ((TaskMaybe2) => {
@@ -2478,6 +2608,7 @@ export {
2478
2608
  Resource,
2479
2609
  Result,
2480
2610
  State,
2611
+ Stream,
2481
2612
  TaskMaybe,
2482
2613
  TaskResult,
2483
2614
  isNonEmptyArr,
package/dist/core.d.mts CHANGED
@@ -2451,6 +2451,166 @@ declare namespace State {
2451
2451
  const focus: <S, A>(lens: Lens<S, A>) => <B>(stateOp: State<A, B>) => State<S, B>;
2452
2452
  }
2453
2453
 
2454
+ /**
2455
+ * An event stream pipeline for a typed message schema `S`.
2456
+ *
2457
+ * `Stream` provides typed event emission, sequence matching, state reduction,
2458
+ * and structural stream forwarding.
2459
+ *
2460
+ * @example
2461
+ * ```ts
2462
+ * type AppMessages = {
2463
+ * userLoggedIn: { userId: string };
2464
+ * checkoutStarted: { amount: number };
2465
+ * };
2466
+ *
2467
+ * const appStream = Stream.make<AppMessages>();
2468
+ *
2469
+ * const subscription = Stream.listen(
2470
+ * appStream,
2471
+ * ["userLoggedIn", "checkoutStarted"],
2472
+ * { ordered: true }
2473
+ * ).reduce(
2474
+ * (msg, state) => {
2475
+ * if (msg.kind === "checkoutStarted") {
2476
+ * return { count: state.count + 1 };
2477
+ * }
2478
+ * return state;
2479
+ * },
2480
+ * { count: 0 }
2481
+ * );
2482
+ *
2483
+ * Stream.emit(appStream, {
2484
+ * kind: "userLoggedIn",
2485
+ * value: { userId: "user-1" },
2486
+ * });
2487
+ * ```
2488
+ */
2489
+ type Stream<S extends Record<string, unknown>> = {
2490
+ readonly options?: Stream.Options;
2491
+ /** @internal */
2492
+ readonly _listeners: Set<(msg: Stream.Message<S>) => void>;
2493
+ /** @internal */
2494
+ readonly _queue: Array<Stream.Message<S>>;
2495
+ /** @internal */
2496
+ _isEmitting: boolean;
2497
+ };
2498
+ declare namespace Stream {
2499
+ /**
2500
+ * Message payload emitted across a Stream.
2501
+ */
2502
+ type Message<S extends Record<string, unknown>> = {
2503
+ [K in keyof S & string]: WithKind<K> & WithValue<S[K]>;
2504
+ }[keyof S & string];
2505
+ /**
2506
+ * Options for constructing a `Stream` instance.
2507
+ */
2508
+ type Options = {
2509
+ readonly name?: string;
2510
+ readonly onError?: (error: unknown) => void;
2511
+ };
2512
+ /**
2513
+ * Options for sequence matching and listener execution.
2514
+ */
2515
+ type SequenceOptions<S extends Record<string, unknown>> = {
2516
+ /** Match events in exact array sequence order (default: false). */
2517
+ readonly ordered?: boolean;
2518
+ /** Match sequence strictly consecutively without intermediary events (default: false). */
2519
+ readonly strict?: boolean;
2520
+ /** Automatically unsubscribe after the first match/cycle completes (default: false). */
2521
+ readonly once?: boolean;
2522
+ /** Event kind(s) that reset sequence tracking to index 0. */
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>;
2526
+ };
2527
+ /**
2528
+ * Handle for an active stateful subscription.
2529
+ */
2530
+ type Subscription<State> = {
2531
+ readonly unsubscribe: () => void;
2532
+ readonly getState: () => State;
2533
+ };
2534
+ /**
2535
+ * Options for structural stream forwarding.
2536
+ */
2537
+ type ForwardOptions<S extends Record<string, unknown>> = {
2538
+ readonly from: Stream<S>;
2539
+ readonly to: Stream<S> | ReadonlyArray<Stream<S>>;
2540
+ readonly only?: ReadonlyArray<keyof S & string>;
2541
+ };
2542
+ /**
2543
+ * Builder handle returned by `Stream.listen`.
2544
+ */
2545
+ type ListenerBuilder<S extends Record<string, unknown>> = {
2546
+ /**
2547
+ * Stateful reduction over events/sequences.
2548
+ */
2549
+ readonly reduce: <State>(reducer: (msg: Message<S>, state: State) => State, initialState: State) => Subscription<State>;
2550
+ /**
2551
+ * Stateless side-effect execution.
2552
+ */
2553
+ readonly tap: (effect: (msg: Message<S>) => void) => () => void;
2554
+ };
2555
+ /**
2556
+ * Constructs a new `Stream` instance.
2557
+ *
2558
+ * @example
2559
+ * ```ts
2560
+ * const stream = Stream.make<AppMessages>({ name: "app" });
2561
+ * ```
2562
+ */
2563
+ const make: <S extends Record<string, unknown>>(options?: Options) => Stream<S>;
2564
+ /**
2565
+ * Emits a message payload to one or more target streams.
2566
+ *
2567
+ * Uses a synchronous breadth-first trampoline queue to handle re-entrant emissions deterministically.
2568
+ *
2569
+ * @example
2570
+ * ```ts
2571
+ * Stream.emit(streamA, {
2572
+ * kind: "userLoggedIn",
2573
+ * value: { userId: "user-1" },
2574
+ * });
2575
+ *
2576
+ * Stream.emit([streamA, streamB], {
2577
+ * kind: "userLoggedIn",
2578
+ * value: { userId: "user-1" },
2579
+ * });
2580
+ * ```
2581
+ */
2582
+ const emit: <S extends Record<string, unknown>, K extends keyof S & string>(target: Stream<S> | ReadonlyArray<Stream<S>>, message: WithKind<K> & WithValue<S[K]>) => void;
2583
+ /**
2584
+ * Forwards messages from one stream to another (or multiple).
2585
+ *
2586
+ * @example
2587
+ * ```ts
2588
+ * const stop = Stream.forward({
2589
+ * from: authStream,
2590
+ * to: analyticsStream,
2591
+ * only: ["userLoggedIn"],
2592
+ * });
2593
+ * ```
2594
+ */
2595
+ const forward: <S extends Record<string, unknown>>(options: ForwardOptions<S>) => () => void;
2596
+ /**
2597
+ * Initiates listener registration on a stream for specific event kind(s) or sequence.
2598
+ *
2599
+ * @example
2600
+ * ```ts
2601
+ * const sub = Stream.listen(
2602
+ * appStream,
2603
+ * ["userLoggedIn", "checkoutStarted"],
2604
+ * { ordered: true }
2605
+ * ).reduce(
2606
+ * (msg, state) => ({ count: state.count + 1 }),
2607
+ * { count: 0 }
2608
+ * );
2609
+ * ```
2610
+ */
2611
+ const listen: <S extends Record<string, unknown>, K extends keyof S & string>(stream: Stream<S>, events: K | ReadonlyArray<K>, options?: SequenceOptions<S>) => ListenerBuilder<S>;
2612
+ }
2613
+
2454
2614
  type TheseFirst<T> = WithKind<"First"> & WithFirst<T>;
2455
2615
  type TheseSecond<T> = WithKind<"Second"> & WithSecond<T>;
2456
2616
  type TheseBoth<First, Second> = WithKind<"Both"> & WithFirst<First> & WithSecond<Second>;
@@ -2859,4 +3019,4 @@ declare namespace Tuple {
2859
3019
  const tap: <A, B>(f: (a: A, b: B) => void) => (tuple: Tuple<A, B>) => Tuple<A, B>;
2860
3020
  }
2861
3021
 
2862
- export { Combinable, Deferred, type Failure, Lazy, Lens, type Loading, Logged, Maybe, type NotAsked, Op, Optional, Predicate, Reader, Refinement, RemoteData, Resource, Result, State, type Success, Task, These, type TheseBoth, type TheseFirst, type TheseSecond, Tuple };
3022
+ export { Combinable, Deferred, type Failure, Lazy, Lens, type Loading, Logged, Maybe, type NotAsked, Op, Optional, Predicate, Reader, Refinement, RemoteData, Resource, Result, State, Stream, type Success, Task, These, type TheseBoth, type TheseFirst, type TheseSecond, Tuple };
package/dist/core.d.ts CHANGED
@@ -2451,6 +2451,166 @@ declare namespace State {
2451
2451
  const focus: <S, A>(lens: Lens<S, A>) => <B>(stateOp: State<A, B>) => State<S, B>;
2452
2452
  }
2453
2453
 
2454
+ /**
2455
+ * An event stream pipeline for a typed message schema `S`.
2456
+ *
2457
+ * `Stream` provides typed event emission, sequence matching, state reduction,
2458
+ * and structural stream forwarding.
2459
+ *
2460
+ * @example
2461
+ * ```ts
2462
+ * type AppMessages = {
2463
+ * userLoggedIn: { userId: string };
2464
+ * checkoutStarted: { amount: number };
2465
+ * };
2466
+ *
2467
+ * const appStream = Stream.make<AppMessages>();
2468
+ *
2469
+ * const subscription = Stream.listen(
2470
+ * appStream,
2471
+ * ["userLoggedIn", "checkoutStarted"],
2472
+ * { ordered: true }
2473
+ * ).reduce(
2474
+ * (msg, state) => {
2475
+ * if (msg.kind === "checkoutStarted") {
2476
+ * return { count: state.count + 1 };
2477
+ * }
2478
+ * return state;
2479
+ * },
2480
+ * { count: 0 }
2481
+ * );
2482
+ *
2483
+ * Stream.emit(appStream, {
2484
+ * kind: "userLoggedIn",
2485
+ * value: { userId: "user-1" },
2486
+ * });
2487
+ * ```
2488
+ */
2489
+ type Stream<S extends Record<string, unknown>> = {
2490
+ readonly options?: Stream.Options;
2491
+ /** @internal */
2492
+ readonly _listeners: Set<(msg: Stream.Message<S>) => void>;
2493
+ /** @internal */
2494
+ readonly _queue: Array<Stream.Message<S>>;
2495
+ /** @internal */
2496
+ _isEmitting: boolean;
2497
+ };
2498
+ declare namespace Stream {
2499
+ /**
2500
+ * Message payload emitted across a Stream.
2501
+ */
2502
+ type Message<S extends Record<string, unknown>> = {
2503
+ [K in keyof S & string]: WithKind<K> & WithValue<S[K]>;
2504
+ }[keyof S & string];
2505
+ /**
2506
+ * Options for constructing a `Stream` instance.
2507
+ */
2508
+ type Options = {
2509
+ readonly name?: string;
2510
+ readonly onError?: (error: unknown) => void;
2511
+ };
2512
+ /**
2513
+ * Options for sequence matching and listener execution.
2514
+ */
2515
+ type SequenceOptions<S extends Record<string, unknown>> = {
2516
+ /** Match events in exact array sequence order (default: false). */
2517
+ readonly ordered?: boolean;
2518
+ /** Match sequence strictly consecutively without intermediary events (default: false). */
2519
+ readonly strict?: boolean;
2520
+ /** Automatically unsubscribe after the first match/cycle completes (default: false). */
2521
+ readonly once?: boolean;
2522
+ /** Event kind(s) that reset sequence tracking to index 0. */
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>;
2526
+ };
2527
+ /**
2528
+ * Handle for an active stateful subscription.
2529
+ */
2530
+ type Subscription<State> = {
2531
+ readonly unsubscribe: () => void;
2532
+ readonly getState: () => State;
2533
+ };
2534
+ /**
2535
+ * Options for structural stream forwarding.
2536
+ */
2537
+ type ForwardOptions<S extends Record<string, unknown>> = {
2538
+ readonly from: Stream<S>;
2539
+ readonly to: Stream<S> | ReadonlyArray<Stream<S>>;
2540
+ readonly only?: ReadonlyArray<keyof S & string>;
2541
+ };
2542
+ /**
2543
+ * Builder handle returned by `Stream.listen`.
2544
+ */
2545
+ type ListenerBuilder<S extends Record<string, unknown>> = {
2546
+ /**
2547
+ * Stateful reduction over events/sequences.
2548
+ */
2549
+ readonly reduce: <State>(reducer: (msg: Message<S>, state: State) => State, initialState: State) => Subscription<State>;
2550
+ /**
2551
+ * Stateless side-effect execution.
2552
+ */
2553
+ readonly tap: (effect: (msg: Message<S>) => void) => () => void;
2554
+ };
2555
+ /**
2556
+ * Constructs a new `Stream` instance.
2557
+ *
2558
+ * @example
2559
+ * ```ts
2560
+ * const stream = Stream.make<AppMessages>({ name: "app" });
2561
+ * ```
2562
+ */
2563
+ const make: <S extends Record<string, unknown>>(options?: Options) => Stream<S>;
2564
+ /**
2565
+ * Emits a message payload to one or more target streams.
2566
+ *
2567
+ * Uses a synchronous breadth-first trampoline queue to handle re-entrant emissions deterministically.
2568
+ *
2569
+ * @example
2570
+ * ```ts
2571
+ * Stream.emit(streamA, {
2572
+ * kind: "userLoggedIn",
2573
+ * value: { userId: "user-1" },
2574
+ * });
2575
+ *
2576
+ * Stream.emit([streamA, streamB], {
2577
+ * kind: "userLoggedIn",
2578
+ * value: { userId: "user-1" },
2579
+ * });
2580
+ * ```
2581
+ */
2582
+ const emit: <S extends Record<string, unknown>, K extends keyof S & string>(target: Stream<S> | ReadonlyArray<Stream<S>>, message: WithKind<K> & WithValue<S[K]>) => void;
2583
+ /**
2584
+ * Forwards messages from one stream to another (or multiple).
2585
+ *
2586
+ * @example
2587
+ * ```ts
2588
+ * const stop = Stream.forward({
2589
+ * from: authStream,
2590
+ * to: analyticsStream,
2591
+ * only: ["userLoggedIn"],
2592
+ * });
2593
+ * ```
2594
+ */
2595
+ const forward: <S extends Record<string, unknown>>(options: ForwardOptions<S>) => () => void;
2596
+ /**
2597
+ * Initiates listener registration on a stream for specific event kind(s) or sequence.
2598
+ *
2599
+ * @example
2600
+ * ```ts
2601
+ * const sub = Stream.listen(
2602
+ * appStream,
2603
+ * ["userLoggedIn", "checkoutStarted"],
2604
+ * { ordered: true }
2605
+ * ).reduce(
2606
+ * (msg, state) => ({ count: state.count + 1 }),
2607
+ * { count: 0 }
2608
+ * );
2609
+ * ```
2610
+ */
2611
+ const listen: <S extends Record<string, unknown>, K extends keyof S & string>(stream: Stream<S>, events: K | ReadonlyArray<K>, options?: SequenceOptions<S>) => ListenerBuilder<S>;
2612
+ }
2613
+
2454
2614
  type TheseFirst<T> = WithKind<"First"> & WithFirst<T>;
2455
2615
  type TheseSecond<T> = WithKind<"Second"> & WithSecond<T>;
2456
2616
  type TheseBoth<First, Second> = WithKind<"Both"> & WithFirst<First> & WithSecond<Second>;
@@ -2859,4 +3019,4 @@ declare namespace Tuple {
2859
3019
  const tap: <A, B>(f: (a: A, b: B) => void) => (tuple: Tuple<A, B>) => Tuple<A, B>;
2860
3020
  }
2861
3021
 
2862
- export { Combinable, Deferred, type Failure, Lazy, Lens, type Loading, Logged, Maybe, type NotAsked, Op, Optional, Predicate, Reader, Refinement, RemoteData, Resource, Result, State, type Success, Task, These, type TheseBoth, type TheseFirst, type TheseSecond, Tuple };
3022
+ export { Combinable, Deferred, type Failure, Lazy, Lens, type Loading, Logged, Maybe, type NotAsked, Op, Optional, Predicate, Reader, Refinement, RemoteData, Resource, Result, State, Stream, type Success, Task, These, type TheseBoth, type TheseFirst, type TheseSecond, Tuple };
package/dist/core.js CHANGED
@@ -37,6 +37,7 @@ __export(Core_exports, {
37
37
  Resource: () => Resource,
38
38
  Result: () => Result,
39
39
  State: () => State,
40
+ Stream: () => Stream,
40
41
  Task: () => Task,
41
42
  TaskMaybe: () => TaskMaybe,
42
43
  TaskResult: () => TaskResult,
@@ -1805,6 +1806,136 @@ var State;
1805
1806
  };
1806
1807
  })(State || (State = {}));
1807
1808
 
1809
+ // src/Core/Stream.ts
1810
+ var Stream;
1811
+ ((Stream2) => {
1812
+ Stream2.make = (options) => ({
1813
+ options,
1814
+ _listeners: /* @__PURE__ */ new Set(),
1815
+ _queue: [],
1816
+ _isEmitting: false
1817
+ });
1818
+ Stream2.emit = (target, message) => {
1819
+ const targets = Array.isArray(target) ? target : [target];
1820
+ const msg = message;
1821
+ for (const stream of targets) {
1822
+ stream._queue.push(msg);
1823
+ if (!stream._isEmitting) {
1824
+ stream._isEmitting = true;
1825
+ try {
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
+ }
1840
+ }
1841
+ } finally {
1842
+ stream._isEmitting = false;
1843
+ }
1844
+ }
1845
+ }
1846
+ };
1847
+ Stream2.forward = (options) => {
1848
+ const targets = Array.isArray(options.to) ? options.to : [options.to];
1849
+ const filterSet = options.only ? new Set(options.only) : null;
1850
+ const handler = (msg) => {
1851
+ if (filterSet !== null && !filterSet.has(msg.kind)) {
1852
+ return;
1853
+ }
1854
+ for (const target of targets) {
1855
+ (0, Stream2.emit)(target, msg);
1856
+ }
1857
+ };
1858
+ options.from._listeners.add(handler);
1859
+ return () => {
1860
+ options.from._listeners.delete(handler);
1861
+ };
1862
+ };
1863
+ Stream2.listen = (stream, events, options) => {
1864
+ const eventList = Array.isArray(events) ? events : [events];
1865
+ const isOrdered = options?.ordered ?? false;
1866
+ const isStrict = options?.strict ?? false;
1867
+ const isOnce = options?.once ?? false;
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;
1870
+ const createMatcher = (onMatch) => {
1871
+ let sequenceIndex = 0;
1872
+ return (msg) => {
1873
+ if (resetKinds !== null && resetKinds.has(msg.kind)) {
1874
+ sequenceIndex = 0;
1875
+ return;
1876
+ }
1877
+ if (!isOrdered) {
1878
+ if (eventList.includes(msg.kind)) {
1879
+ onMatch(msg);
1880
+ }
1881
+ return;
1882
+ }
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
+ }
1894
+ if (msg.kind === expectedKind) {
1895
+ sequenceIndex++;
1896
+ if (sequenceIndex === eventList.length) {
1897
+ sequenceIndex = 0;
1898
+ onMatch(msg);
1899
+ }
1900
+ } else if (isStrict) {
1901
+ sequenceIndex = msg.kind === eventList[0] ? 1 : 0;
1902
+ } else if (eventList.includes(msg.kind)) {
1903
+ sequenceIndex = msg.kind === eventList[0] ? 1 : 0;
1904
+ }
1905
+ };
1906
+ };
1907
+ return {
1908
+ reduce: (reducer, initialState) => {
1909
+ let currentState = initialState;
1910
+ const listenerFn = createMatcher((msg) => {
1911
+ currentState = reducer(msg, currentState);
1912
+ if (isOnce) {
1913
+ stream._listeners.delete(listenerFn);
1914
+ }
1915
+ });
1916
+ const unsubscribe = () => {
1917
+ stream._listeners.delete(listenerFn);
1918
+ };
1919
+ stream._listeners.add(listenerFn);
1920
+ return { unsubscribe, getState: () => currentState };
1921
+ },
1922
+ tap: (effect) => {
1923
+ const listenerFn = createMatcher((msg) => {
1924
+ effect(msg);
1925
+ if (isOnce) {
1926
+ stream._listeners.delete(listenerFn);
1927
+ }
1928
+ });
1929
+ const unsubscribe = () => {
1930
+ stream._listeners.delete(listenerFn);
1931
+ };
1932
+ stream._listeners.add(listenerFn);
1933
+ return unsubscribe;
1934
+ }
1935
+ };
1936
+ };
1937
+ })(Stream || (Stream = {}));
1938
+
1808
1939
  // src/Core/TaskMaybe.ts
1809
1940
  var TaskMaybe;
1810
1941
  ((TaskMaybe2) => {
@@ -2551,6 +2682,7 @@ var Validation;
2551
2682
  Resource,
2552
2683
  Result,
2553
2684
  State,
2685
+ Stream,
2554
2686
  Task,
2555
2687
  TaskMaybe,
2556
2688
  TaskResult,
package/dist/core.mjs CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  Resource,
17
17
  Result,
18
18
  State,
19
+ Stream,
19
20
  Task,
20
21
  TaskMaybe,
21
22
  TaskResult,
@@ -23,7 +24,7 @@ import {
23
24
  These,
24
25
  Tuple,
25
26
  Validation
26
- } from "./chunk-LR63GW6J.mjs";
27
+ } from "./chunk-U64ASY7P.mjs";
27
28
  import "./chunk-DENXUTKL.mjs";
28
29
  export {
29
30
  Combinable,
@@ -43,6 +44,7 @@ export {
43
44
  Resource,
44
45
  Result,
45
46
  State,
47
+ Stream,
46
48
  Task,
47
49
  TaskMaybe,
48
50
  TaskResult,
package/dist/data.mjs CHANGED
@@ -7,8 +7,8 @@ import {
7
7
  Rec,
8
8
  Str,
9
9
  Uniq
10
- } from "./chunk-UGVU2RTM.mjs";
11
- import "./chunk-LR63GW6J.mjs";
10
+ } from "./chunk-6N27QZFK.mjs";
11
+ import "./chunk-U64ASY7P.mjs";
12
12
  import "./chunk-DENXUTKL.mjs";
13
13
  export {
14
14
  Arr,
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { and, compose, constFalse, constNull, constTrue, constUndefined, constVoid, constant, converge, curry, curry3, curry4, defaultTo, flip, flow, identity, juxt, memoize, memoizeWeak, not, on, once, or, pipe, tap, tuple, uncurry, uncurry3, uncurry4, untuple } from './composition.mjs';
2
- export { Combinable, Failure, Lazy, Lens, Loading, Logged, NotAsked, Op, Optional, Predicate, Reader, Refinement, RemoteData, Resource, State, Success, These, TheseBoth, TheseFirst, TheseSecond, Tuple } from './core.mjs';
2
+ export { Combinable, Failure, Lazy, Lens, Loading, Logged, NotAsked, Op, Optional, Predicate, Reader, Refinement, RemoteData, Resource, State, Stream, Success, These, TheseBoth, TheseFirst, TheseSecond, Tuple } from './core.mjs';
3
3
  export { D as Deferred } from './InternalTypes-CDiDBAY4.mjs';
4
4
  export { E as Equality, a as Err, F as Failed, M as Maybe, N as None, O as Ok, b as Ordering, P as Passed, R as Result, S as Some, T as Task, c as TaskMaybe, d as TaskResult, e as TaskValidation, V as Validation } from './Validation-D-aARYlP.mjs';
5
5
  export { Arr, BigNum, Dict, Json, NonEmptyMap, NonEmptyRecord, NonEmptySet, NonEmptyString, Num, Rec, Str, Uniq } from './data.mjs';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { and, compose, constFalse, constNull, constTrue, constUndefined, constVoid, constant, converge, curry, curry3, curry4, defaultTo, flip, flow, identity, juxt, memoize, memoizeWeak, not, on, once, or, pipe, tap, tuple, uncurry, uncurry3, uncurry4, untuple } from './composition.js';
2
- export { Combinable, Failure, Lazy, Lens, Loading, Logged, NotAsked, Op, Optional, Predicate, Reader, Refinement, RemoteData, Resource, State, Success, These, TheseBoth, TheseFirst, TheseSecond, Tuple } from './core.js';
2
+ export { Combinable, Failure, Lazy, Lens, Loading, Logged, NotAsked, Op, Optional, Predicate, Reader, Refinement, RemoteData, Resource, State, Stream, Success, These, TheseBoth, TheseFirst, TheseSecond, Tuple } from './core.js';
3
3
  export { D as Deferred } from './InternalTypes-LdhLQx3N.js';
4
4
  export { E as Equality, a as Err, F as Failed, M as Maybe, N as None, O as Ok, b as Ordering, P as Passed, R as Result, S as Some, T as Task, c as TaskMaybe, d as TaskResult, e as TaskValidation, V as Validation } from './Validation-1OgJJdeA.js';
5
5
  export { Arr, BigNum, Dict, Json, NonEmptyMap, NonEmptyRecord, NonEmptySet, NonEmptyString, Num, Rec, Str, Uniq } from './data.js';
package/dist/index.js CHANGED
@@ -47,6 +47,7 @@ __export(src_exports, {
47
47
  RetryPolicy: () => RetryPolicy,
48
48
  State: () => State,
49
49
  Str: () => Str,
50
+ Stream: () => Stream,
50
51
  Task: () => Task,
51
52
  TaskMaybe: () => TaskMaybe,
52
53
  TaskResult: () => TaskResult,
@@ -2287,6 +2288,136 @@ var State;
2287
2288
  };
2288
2289
  })(State || (State = {}));
2289
2290
 
2291
+ // src/Core/Stream.ts
2292
+ var Stream;
2293
+ ((Stream2) => {
2294
+ Stream2.make = (options) => ({
2295
+ options,
2296
+ _listeners: /* @__PURE__ */ new Set(),
2297
+ _queue: [],
2298
+ _isEmitting: false
2299
+ });
2300
+ Stream2.emit = (target, message) => {
2301
+ const targets = Array.isArray(target) ? target : [target];
2302
+ const msg = message;
2303
+ for (const stream of targets) {
2304
+ stream._queue.push(msg);
2305
+ if (!stream._isEmitting) {
2306
+ stream._isEmitting = true;
2307
+ try {
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
+ }
2322
+ }
2323
+ } finally {
2324
+ stream._isEmitting = false;
2325
+ }
2326
+ }
2327
+ }
2328
+ };
2329
+ Stream2.forward = (options) => {
2330
+ const targets = Array.isArray(options.to) ? options.to : [options.to];
2331
+ const filterSet = options.only ? new Set(options.only) : null;
2332
+ const handler = (msg) => {
2333
+ if (filterSet !== null && !filterSet.has(msg.kind)) {
2334
+ return;
2335
+ }
2336
+ for (const target of targets) {
2337
+ (0, Stream2.emit)(target, msg);
2338
+ }
2339
+ };
2340
+ options.from._listeners.add(handler);
2341
+ return () => {
2342
+ options.from._listeners.delete(handler);
2343
+ };
2344
+ };
2345
+ Stream2.listen = (stream, events, options) => {
2346
+ const eventList = Array.isArray(events) ? events : [events];
2347
+ const isOrdered = options?.ordered ?? false;
2348
+ const isStrict = options?.strict ?? false;
2349
+ const isOnce = options?.once ?? false;
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;
2352
+ const createMatcher = (onMatch) => {
2353
+ let sequenceIndex = 0;
2354
+ return (msg) => {
2355
+ if (resetKinds !== null && resetKinds.has(msg.kind)) {
2356
+ sequenceIndex = 0;
2357
+ return;
2358
+ }
2359
+ if (!isOrdered) {
2360
+ if (eventList.includes(msg.kind)) {
2361
+ onMatch(msg);
2362
+ }
2363
+ return;
2364
+ }
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
+ }
2376
+ if (msg.kind === expectedKind) {
2377
+ sequenceIndex++;
2378
+ if (sequenceIndex === eventList.length) {
2379
+ sequenceIndex = 0;
2380
+ onMatch(msg);
2381
+ }
2382
+ } else if (isStrict) {
2383
+ sequenceIndex = msg.kind === eventList[0] ? 1 : 0;
2384
+ } else if (eventList.includes(msg.kind)) {
2385
+ sequenceIndex = msg.kind === eventList[0] ? 1 : 0;
2386
+ }
2387
+ };
2388
+ };
2389
+ return {
2390
+ reduce: (reducer, initialState) => {
2391
+ let currentState = initialState;
2392
+ const listenerFn = createMatcher((msg) => {
2393
+ currentState = reducer(msg, currentState);
2394
+ if (isOnce) {
2395
+ stream._listeners.delete(listenerFn);
2396
+ }
2397
+ });
2398
+ const unsubscribe = () => {
2399
+ stream._listeners.delete(listenerFn);
2400
+ };
2401
+ stream._listeners.add(listenerFn);
2402
+ return { unsubscribe, getState: () => currentState };
2403
+ },
2404
+ tap: (effect) => {
2405
+ const listenerFn = createMatcher((msg) => {
2406
+ effect(msg);
2407
+ if (isOnce) {
2408
+ stream._listeners.delete(listenerFn);
2409
+ }
2410
+ });
2411
+ const unsubscribe = () => {
2412
+ stream._listeners.delete(listenerFn);
2413
+ };
2414
+ stream._listeners.add(listenerFn);
2415
+ return unsubscribe;
2416
+ }
2417
+ };
2418
+ };
2419
+ })(Stream || (Stream = {}));
2420
+
2290
2421
  // src/Core/TaskMaybe.ts
2291
2422
  var TaskMaybe;
2292
2423
  ((TaskMaybe2) => {
@@ -4350,6 +4481,7 @@ var Uniq;
4350
4481
  RetryPolicy,
4351
4482
  State,
4352
4483
  Str,
4484
+ Stream,
4353
4485
  Task,
4354
4486
  TaskMaybe,
4355
4487
  TaskResult,
package/dist/index.mjs CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  Rec,
40
40
  Str,
41
41
  Uniq
42
- } from "./chunk-UGVU2RTM.mjs";
42
+ } from "./chunk-6N27QZFK.mjs";
43
43
  import {
44
44
  Combinable,
45
45
  Deferred,
@@ -58,6 +58,7 @@ import {
58
58
  Resource,
59
59
  Result,
60
60
  State,
61
+ Stream,
61
62
  Task,
62
63
  TaskMaybe,
63
64
  TaskResult,
@@ -65,7 +66,7 @@ import {
65
66
  These,
66
67
  Tuple,
67
68
  Validation
68
- } from "./chunk-LR63GW6J.mjs";
69
+ } from "./chunk-U64ASY7P.mjs";
69
70
  import {
70
71
  Brand,
71
72
  Duration,
@@ -99,6 +100,7 @@ export {
99
100
  RetryPolicy,
100
101
  State,
101
102
  Str,
103
+ Stream,
102
104
  Task,
103
105
  TaskMaybe,
104
106
  TaskResult,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nlozgachev/pipelined",
3
- "version": "0.48.0",
3
+ "version": "0.50.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"
@@ -68,24 +68,44 @@
68
68
  "docs:build": "pnpm --filter pipelined-docs build"
69
69
  },
70
70
  "devDependencies": {
71
- "@size-limit/file": "12.1.0",
72
- "@types/node": "25.9.3",
73
- "@vitest/coverage-v8": "4.1.8",
74
- "bumpp": "11.1.0",
75
- "dprint": "0.54.0",
76
- "fast-check": "4.8.0",
77
- "oxlint": "1.69.0",
78
- "size-limit": "12.1.0",
71
+ "@size-limit/file": "13.0.3",
72
+ "@types/node": "26.2.0",
73
+ "@vitest/coverage-v8": "4.1.10",
74
+ "bumpp": "12.2.0",
75
+ "dprint": "0.55.2",
76
+ "fast-check": "4.9.0",
77
+ "oxlint": "1.78.0",
78
+ "size-limit": "13.0.3",
79
79
  "tsup": "8.5.1",
80
80
  "typescript": "6.0.3",
81
- "vitest": "4.1.8"
81
+ "vitest": "4.1.10"
82
82
  },
83
83
  "size-limit": [
84
- { "path": "dist/index.js", "limit": "25 KB", "gzip": true },
85
- { "path": "dist/core.js", "limit": "15 KB", "gzip": true },
86
- { "path": "dist/composition.js", "limit": "4 KB", "gzip": true },
87
- { "path": "dist/data.js", "limit": "12 KB", "gzip": true },
88
- { "path": "dist/types.js", "limit": "1 KB", "gzip": true }
84
+ {
85
+ "path": "dist/index.js",
86
+ "limit": "25 KB",
87
+ "gzip": true
88
+ },
89
+ {
90
+ "path": "dist/core.js",
91
+ "limit": "16 KB",
92
+ "gzip": true
93
+ },
94
+ {
95
+ "path": "dist/composition.js",
96
+ "limit": "4 KB",
97
+ "gzip": true
98
+ },
99
+ {
100
+ "path": "dist/data.js",
101
+ "limit": "13 KB",
102
+ "gzip": true
103
+ },
104
+ {
105
+ "path": "dist/types.js",
106
+ "limit": "2 KB",
107
+ "gzip": true
108
+ }
89
109
  ],
90
110
  "packageManager": "pnpm@11.6.0+sha512.9a36518224080c6fe5165afdcfe79bfa118c29be703f3f462b1e32efe1e98e47e8750b148e08286250aad4113cc7993ca413c4e2cd447752708c2ee5751bc95f"
91
111
  }