@nlozgachev/pipelined 0.48.0 → 0.49.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-IHLH2XYQ.mjs";
8
8
 
9
9
  // src/Data/Arr.ts
10
10
  var ArrMaybe;
@@ -1732,6 +1732,112 @@ 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
+ });
1742
+ Stream2.emit = (target, message) => {
1743
+ const targets = Array.isArray(target) ? target : [target];
1744
+ const msg = message;
1745
+ for (const stream of targets) {
1746
+ const listeners = Array.from(stream._listeners);
1747
+ for (const listener of listeners) {
1748
+ try {
1749
+ listener(msg);
1750
+ } catch (err2) {
1751
+ if (stream.options?.onError) {
1752
+ stream.options.onError(err2);
1753
+ } else {
1754
+ throw err2;
1755
+ }
1756
+ }
1757
+ }
1758
+ }
1759
+ };
1760
+ Stream2.forward = (options) => {
1761
+ const targets = Array.isArray(options.to) ? options.to : [options.to];
1762
+ const filterSet = options.only ? new Set(options.only) : null;
1763
+ const handler = (msg) => {
1764
+ if (filterSet !== null && !filterSet.has(msg.kind)) {
1765
+ return;
1766
+ }
1767
+ for (const target of targets) {
1768
+ (0, Stream2.emit)(target, msg);
1769
+ }
1770
+ };
1771
+ options.from._listeners.add(handler);
1772
+ return () => {
1773
+ options.from._listeners.delete(handler);
1774
+ };
1775
+ };
1776
+ Stream2.listen = (stream, events, options) => {
1777
+ const eventList = Array.isArray(events) ? events : [events];
1778
+ const isOrdered = options?.ordered ?? false;
1779
+ const isStrict = options?.strict ?? false;
1780
+ const isOnce = options?.once ?? false;
1781
+ const resetKinds = options?.reset ? new Set(Array.isArray(options.reset) ? options.reset : [options.reset]) : null;
1782
+ const createMatcher = (onMatch) => {
1783
+ let sequenceIndex = 0;
1784
+ return (msg) => {
1785
+ if (resetKinds !== null && resetKinds.has(msg.kind)) {
1786
+ sequenceIndex = 0;
1787
+ return;
1788
+ }
1789
+ if (!isOrdered) {
1790
+ if (eventList.includes(msg.kind)) {
1791
+ onMatch(msg);
1792
+ }
1793
+ return;
1794
+ }
1795
+ const expectedKind = eventList[sequenceIndex];
1796
+ if (msg.kind === expectedKind) {
1797
+ sequenceIndex++;
1798
+ if (sequenceIndex === eventList.length) {
1799
+ sequenceIndex = 0;
1800
+ onMatch(msg);
1801
+ }
1802
+ } else if (isStrict) {
1803
+ sequenceIndex = msg.kind === eventList[0] ? 1 : 0;
1804
+ } else if (eventList.includes(msg.kind)) {
1805
+ sequenceIndex = msg.kind === eventList[0] ? 1 : 0;
1806
+ }
1807
+ };
1808
+ };
1809
+ return {
1810
+ reduce: (reducer, initialState) => {
1811
+ let currentState = initialState;
1812
+ const listenerFn = createMatcher((msg) => {
1813
+ currentState = reducer(msg, currentState);
1814
+ if (isOnce) {
1815
+ stream._listeners.delete(listenerFn);
1816
+ }
1817
+ });
1818
+ const unsubscribe = () => {
1819
+ stream._listeners.delete(listenerFn);
1820
+ };
1821
+ stream._listeners.add(listenerFn);
1822
+ return { unsubscribe, getState: () => currentState };
1823
+ },
1824
+ tap: (effect) => {
1825
+ const listenerFn = createMatcher((msg) => {
1826
+ effect(msg);
1827
+ if (isOnce) {
1828
+ stream._listeners.delete(listenerFn);
1829
+ }
1830
+ });
1831
+ const unsubscribe = () => {
1832
+ stream._listeners.delete(listenerFn);
1833
+ };
1834
+ stream._listeners.add(listenerFn);
1835
+ return unsubscribe;
1836
+ }
1837
+ };
1838
+ };
1839
+ })(Stream || (Stream = {}));
1840
+
1735
1841
  // src/Core/TaskMaybe.ts
1736
1842
  var TaskMaybe;
1737
1843
  ((TaskMaybe2) => {
@@ -2478,6 +2584,7 @@ export {
2478
2584
  Resource,
2479
2585
  Result,
2480
2586
  State,
2587
+ Stream,
2481
2588
  TaskMaybe,
2482
2589
  TaskResult,
2483
2590
  isNonEmptyArr,
package/dist/core.d.mts CHANGED
@@ -2451,6 +2451,158 @@ 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
+ };
2494
+ declare namespace Stream {
2495
+ /**
2496
+ * Message payload emitted across a Stream.
2497
+ */
2498
+ type Message<S extends Record<string, unknown>> = {
2499
+ [K in keyof S & string]: WithKind<K> & WithValue<S[K]>;
2500
+ }[keyof S & string];
2501
+ /**
2502
+ * Options for constructing a `Stream` instance.
2503
+ */
2504
+ type Options = {
2505
+ readonly name?: string;
2506
+ readonly onError?: (error: unknown) => void;
2507
+ };
2508
+ /**
2509
+ * Options for sequence matching and listener execution.
2510
+ */
2511
+ type SequenceOptions<S extends Record<string, unknown>> = {
2512
+ /** Match events in exact array sequence order (default: false). */
2513
+ readonly ordered?: boolean;
2514
+ /** Match sequence strictly consecutively without intermediary events (default: false). */
2515
+ readonly strict?: boolean;
2516
+ /** Automatically unsubscribe after the first match/cycle completes (default: false). */
2517
+ readonly once?: boolean;
2518
+ /** Event kind(s) that reset sequence tracking to index 0. */
2519
+ readonly reset?: (keyof S & string) | ReadonlyArray<keyof S & string>;
2520
+ };
2521
+ /**
2522
+ * Handle for an active stateful subscription.
2523
+ */
2524
+ type Subscription<State> = {
2525
+ readonly unsubscribe: () => void;
2526
+ readonly getState: () => State;
2527
+ };
2528
+ /**
2529
+ * Options for structural stream forwarding.
2530
+ */
2531
+ type ForwardOptions<S extends Record<string, unknown>> = {
2532
+ readonly from: Stream<S>;
2533
+ readonly to: Stream<S> | ReadonlyArray<Stream<S>>;
2534
+ readonly only?: ReadonlyArray<keyof S & string>;
2535
+ };
2536
+ /**
2537
+ * Builder handle returned by `Stream.listen`.
2538
+ */
2539
+ type ListenerBuilder<S extends Record<string, unknown>> = {
2540
+ /**
2541
+ * Stateful reduction over events/sequences.
2542
+ */
2543
+ readonly reduce: <State>(reducer: (msg: Message<S>, state: State) => State, initialState: State) => Subscription<State>;
2544
+ /**
2545
+ * Stateless side-effect execution.
2546
+ */
2547
+ readonly tap: (effect: (msg: Message<S>) => void) => () => void;
2548
+ };
2549
+ /**
2550
+ * Constructs a new `Stream` instance.
2551
+ *
2552
+ * @example
2553
+ * ```ts
2554
+ * const stream = Stream.make<AppMessages>({ name: "app" });
2555
+ * ```
2556
+ */
2557
+ const make: <S extends Record<string, unknown>>(options?: Options) => Stream<S>;
2558
+ /**
2559
+ * Emits a message payload to one or more target streams.
2560
+ *
2561
+ * @example
2562
+ * ```ts
2563
+ * Stream.emit(streamA, {
2564
+ * kind: "userLoggedIn",
2565
+ * value: { userId: "user-1" },
2566
+ * });
2567
+ *
2568
+ * Stream.emit([streamA, streamB], {
2569
+ * kind: "userLoggedIn",
2570
+ * value: { userId: "user-1" },
2571
+ * });
2572
+ * ```
2573
+ */
2574
+ 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;
2575
+ /**
2576
+ * Forwards messages from one stream to another (or multiple).
2577
+ *
2578
+ * @example
2579
+ * ```ts
2580
+ * const stop = Stream.forward({
2581
+ * from: authStream,
2582
+ * to: analyticsStream,
2583
+ * only: ["userLoggedIn"],
2584
+ * });
2585
+ * ```
2586
+ */
2587
+ const forward: <S extends Record<string, unknown>>(options: ForwardOptions<S>) => () => void;
2588
+ /**
2589
+ * Initiates listener registration on a stream for specific event kind(s) or sequence.
2590
+ *
2591
+ * @example
2592
+ * ```ts
2593
+ * const sub = Stream.listen(
2594
+ * appStream,
2595
+ * ["userLoggedIn", "checkoutStarted"],
2596
+ * { ordered: true }
2597
+ * ).reduce(
2598
+ * (msg, state) => ({ count: state.count + 1 }),
2599
+ * { count: 0 }
2600
+ * );
2601
+ * ```
2602
+ */
2603
+ const listen: <S extends Record<string, unknown>, K extends keyof S & string>(stream: Stream<S>, events: K | ReadonlyArray<K>, options?: SequenceOptions<S>) => ListenerBuilder<S>;
2604
+ }
2605
+
2454
2606
  type TheseFirst<T> = WithKind<"First"> & WithFirst<T>;
2455
2607
  type TheseSecond<T> = WithKind<"Second"> & WithSecond<T>;
2456
2608
  type TheseBoth<First, Second> = WithKind<"Both"> & WithFirst<First> & WithSecond<Second>;
@@ -2859,4 +3011,4 @@ declare namespace Tuple {
2859
3011
  const tap: <A, B>(f: (a: A, b: B) => void) => (tuple: Tuple<A, B>) => Tuple<A, B>;
2860
3012
  }
2861
3013
 
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 };
3014
+ 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,158 @@ 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
+ };
2494
+ declare namespace Stream {
2495
+ /**
2496
+ * Message payload emitted across a Stream.
2497
+ */
2498
+ type Message<S extends Record<string, unknown>> = {
2499
+ [K in keyof S & string]: WithKind<K> & WithValue<S[K]>;
2500
+ }[keyof S & string];
2501
+ /**
2502
+ * Options for constructing a `Stream` instance.
2503
+ */
2504
+ type Options = {
2505
+ readonly name?: string;
2506
+ readonly onError?: (error: unknown) => void;
2507
+ };
2508
+ /**
2509
+ * Options for sequence matching and listener execution.
2510
+ */
2511
+ type SequenceOptions<S extends Record<string, unknown>> = {
2512
+ /** Match events in exact array sequence order (default: false). */
2513
+ readonly ordered?: boolean;
2514
+ /** Match sequence strictly consecutively without intermediary events (default: false). */
2515
+ readonly strict?: boolean;
2516
+ /** Automatically unsubscribe after the first match/cycle completes (default: false). */
2517
+ readonly once?: boolean;
2518
+ /** Event kind(s) that reset sequence tracking to index 0. */
2519
+ readonly reset?: (keyof S & string) | ReadonlyArray<keyof S & string>;
2520
+ };
2521
+ /**
2522
+ * Handle for an active stateful subscription.
2523
+ */
2524
+ type Subscription<State> = {
2525
+ readonly unsubscribe: () => void;
2526
+ readonly getState: () => State;
2527
+ };
2528
+ /**
2529
+ * Options for structural stream forwarding.
2530
+ */
2531
+ type ForwardOptions<S extends Record<string, unknown>> = {
2532
+ readonly from: Stream<S>;
2533
+ readonly to: Stream<S> | ReadonlyArray<Stream<S>>;
2534
+ readonly only?: ReadonlyArray<keyof S & string>;
2535
+ };
2536
+ /**
2537
+ * Builder handle returned by `Stream.listen`.
2538
+ */
2539
+ type ListenerBuilder<S extends Record<string, unknown>> = {
2540
+ /**
2541
+ * Stateful reduction over events/sequences.
2542
+ */
2543
+ readonly reduce: <State>(reducer: (msg: Message<S>, state: State) => State, initialState: State) => Subscription<State>;
2544
+ /**
2545
+ * Stateless side-effect execution.
2546
+ */
2547
+ readonly tap: (effect: (msg: Message<S>) => void) => () => void;
2548
+ };
2549
+ /**
2550
+ * Constructs a new `Stream` instance.
2551
+ *
2552
+ * @example
2553
+ * ```ts
2554
+ * const stream = Stream.make<AppMessages>({ name: "app" });
2555
+ * ```
2556
+ */
2557
+ const make: <S extends Record<string, unknown>>(options?: Options) => Stream<S>;
2558
+ /**
2559
+ * Emits a message payload to one or more target streams.
2560
+ *
2561
+ * @example
2562
+ * ```ts
2563
+ * Stream.emit(streamA, {
2564
+ * kind: "userLoggedIn",
2565
+ * value: { userId: "user-1" },
2566
+ * });
2567
+ *
2568
+ * Stream.emit([streamA, streamB], {
2569
+ * kind: "userLoggedIn",
2570
+ * value: { userId: "user-1" },
2571
+ * });
2572
+ * ```
2573
+ */
2574
+ 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;
2575
+ /**
2576
+ * Forwards messages from one stream to another (or multiple).
2577
+ *
2578
+ * @example
2579
+ * ```ts
2580
+ * const stop = Stream.forward({
2581
+ * from: authStream,
2582
+ * to: analyticsStream,
2583
+ * only: ["userLoggedIn"],
2584
+ * });
2585
+ * ```
2586
+ */
2587
+ const forward: <S extends Record<string, unknown>>(options: ForwardOptions<S>) => () => void;
2588
+ /**
2589
+ * Initiates listener registration on a stream for specific event kind(s) or sequence.
2590
+ *
2591
+ * @example
2592
+ * ```ts
2593
+ * const sub = Stream.listen(
2594
+ * appStream,
2595
+ * ["userLoggedIn", "checkoutStarted"],
2596
+ * { ordered: true }
2597
+ * ).reduce(
2598
+ * (msg, state) => ({ count: state.count + 1 }),
2599
+ * { count: 0 }
2600
+ * );
2601
+ * ```
2602
+ */
2603
+ const listen: <S extends Record<string, unknown>, K extends keyof S & string>(stream: Stream<S>, events: K | ReadonlyArray<K>, options?: SequenceOptions<S>) => ListenerBuilder<S>;
2604
+ }
2605
+
2454
2606
  type TheseFirst<T> = WithKind<"First"> & WithFirst<T>;
2455
2607
  type TheseSecond<T> = WithKind<"Second"> & WithSecond<T>;
2456
2608
  type TheseBoth<First, Second> = WithKind<"Both"> & WithFirst<First> & WithSecond<Second>;
@@ -2859,4 +3011,4 @@ declare namespace Tuple {
2859
3011
  const tap: <A, B>(f: (a: A, b: B) => void) => (tuple: Tuple<A, B>) => Tuple<A, B>;
2860
3012
  }
2861
3013
 
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 };
3014
+ 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,112 @@ 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
+ });
1816
+ Stream2.emit = (target, message) => {
1817
+ const targets = Array.isArray(target) ? target : [target];
1818
+ const msg = message;
1819
+ for (const stream of targets) {
1820
+ const listeners = Array.from(stream._listeners);
1821
+ for (const listener of listeners) {
1822
+ try {
1823
+ listener(msg);
1824
+ } catch (err2) {
1825
+ if (stream.options?.onError) {
1826
+ stream.options.onError(err2);
1827
+ } else {
1828
+ throw err2;
1829
+ }
1830
+ }
1831
+ }
1832
+ }
1833
+ };
1834
+ Stream2.forward = (options) => {
1835
+ const targets = Array.isArray(options.to) ? options.to : [options.to];
1836
+ const filterSet = options.only ? new Set(options.only) : null;
1837
+ const handler = (msg) => {
1838
+ if (filterSet !== null && !filterSet.has(msg.kind)) {
1839
+ return;
1840
+ }
1841
+ for (const target of targets) {
1842
+ (0, Stream2.emit)(target, msg);
1843
+ }
1844
+ };
1845
+ options.from._listeners.add(handler);
1846
+ return () => {
1847
+ options.from._listeners.delete(handler);
1848
+ };
1849
+ };
1850
+ Stream2.listen = (stream, events, options) => {
1851
+ const eventList = Array.isArray(events) ? events : [events];
1852
+ const isOrdered = options?.ordered ?? false;
1853
+ const isStrict = options?.strict ?? false;
1854
+ const isOnce = options?.once ?? false;
1855
+ const resetKinds = options?.reset ? new Set(Array.isArray(options.reset) ? options.reset : [options.reset]) : null;
1856
+ const createMatcher = (onMatch) => {
1857
+ let sequenceIndex = 0;
1858
+ return (msg) => {
1859
+ if (resetKinds !== null && resetKinds.has(msg.kind)) {
1860
+ sequenceIndex = 0;
1861
+ return;
1862
+ }
1863
+ if (!isOrdered) {
1864
+ if (eventList.includes(msg.kind)) {
1865
+ onMatch(msg);
1866
+ }
1867
+ return;
1868
+ }
1869
+ const expectedKind = eventList[sequenceIndex];
1870
+ if (msg.kind === expectedKind) {
1871
+ sequenceIndex++;
1872
+ if (sequenceIndex === eventList.length) {
1873
+ sequenceIndex = 0;
1874
+ onMatch(msg);
1875
+ }
1876
+ } else if (isStrict) {
1877
+ sequenceIndex = msg.kind === eventList[0] ? 1 : 0;
1878
+ } else if (eventList.includes(msg.kind)) {
1879
+ sequenceIndex = msg.kind === eventList[0] ? 1 : 0;
1880
+ }
1881
+ };
1882
+ };
1883
+ return {
1884
+ reduce: (reducer, initialState) => {
1885
+ let currentState = initialState;
1886
+ const listenerFn = createMatcher((msg) => {
1887
+ currentState = reducer(msg, currentState);
1888
+ if (isOnce) {
1889
+ stream._listeners.delete(listenerFn);
1890
+ }
1891
+ });
1892
+ const unsubscribe = () => {
1893
+ stream._listeners.delete(listenerFn);
1894
+ };
1895
+ stream._listeners.add(listenerFn);
1896
+ return { unsubscribe, getState: () => currentState };
1897
+ },
1898
+ tap: (effect) => {
1899
+ const listenerFn = createMatcher((msg) => {
1900
+ effect(msg);
1901
+ if (isOnce) {
1902
+ stream._listeners.delete(listenerFn);
1903
+ }
1904
+ });
1905
+ const unsubscribe = () => {
1906
+ stream._listeners.delete(listenerFn);
1907
+ };
1908
+ stream._listeners.add(listenerFn);
1909
+ return unsubscribe;
1910
+ }
1911
+ };
1912
+ };
1913
+ })(Stream || (Stream = {}));
1914
+
1808
1915
  // src/Core/TaskMaybe.ts
1809
1916
  var TaskMaybe;
1810
1917
  ((TaskMaybe2) => {
@@ -2551,6 +2658,7 @@ var Validation;
2551
2658
  Resource,
2552
2659
  Result,
2553
2660
  State,
2661
+ Stream,
2554
2662
  Task,
2555
2663
  TaskMaybe,
2556
2664
  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-IHLH2XYQ.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-EPT3GYGN.mjs";
11
+ import "./chunk-IHLH2XYQ.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,112 @@ 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
+ });
2298
+ Stream2.emit = (target, message) => {
2299
+ const targets = Array.isArray(target) ? target : [target];
2300
+ const msg = message;
2301
+ for (const stream of targets) {
2302
+ const listeners = Array.from(stream._listeners);
2303
+ for (const listener of listeners) {
2304
+ try {
2305
+ listener(msg);
2306
+ } catch (err2) {
2307
+ if (stream.options?.onError) {
2308
+ stream.options.onError(err2);
2309
+ } else {
2310
+ throw err2;
2311
+ }
2312
+ }
2313
+ }
2314
+ }
2315
+ };
2316
+ Stream2.forward = (options) => {
2317
+ const targets = Array.isArray(options.to) ? options.to : [options.to];
2318
+ const filterSet = options.only ? new Set(options.only) : null;
2319
+ const handler = (msg) => {
2320
+ if (filterSet !== null && !filterSet.has(msg.kind)) {
2321
+ return;
2322
+ }
2323
+ for (const target of targets) {
2324
+ (0, Stream2.emit)(target, msg);
2325
+ }
2326
+ };
2327
+ options.from._listeners.add(handler);
2328
+ return () => {
2329
+ options.from._listeners.delete(handler);
2330
+ };
2331
+ };
2332
+ Stream2.listen = (stream, events, options) => {
2333
+ const eventList = Array.isArray(events) ? events : [events];
2334
+ const isOrdered = options?.ordered ?? false;
2335
+ const isStrict = options?.strict ?? false;
2336
+ const isOnce = options?.once ?? false;
2337
+ const resetKinds = options?.reset ? new Set(Array.isArray(options.reset) ? options.reset : [options.reset]) : null;
2338
+ const createMatcher = (onMatch) => {
2339
+ let sequenceIndex = 0;
2340
+ return (msg) => {
2341
+ if (resetKinds !== null && resetKinds.has(msg.kind)) {
2342
+ sequenceIndex = 0;
2343
+ return;
2344
+ }
2345
+ if (!isOrdered) {
2346
+ if (eventList.includes(msg.kind)) {
2347
+ onMatch(msg);
2348
+ }
2349
+ return;
2350
+ }
2351
+ const expectedKind = eventList[sequenceIndex];
2352
+ if (msg.kind === expectedKind) {
2353
+ sequenceIndex++;
2354
+ if (sequenceIndex === eventList.length) {
2355
+ sequenceIndex = 0;
2356
+ onMatch(msg);
2357
+ }
2358
+ } else if (isStrict) {
2359
+ sequenceIndex = msg.kind === eventList[0] ? 1 : 0;
2360
+ } else if (eventList.includes(msg.kind)) {
2361
+ sequenceIndex = msg.kind === eventList[0] ? 1 : 0;
2362
+ }
2363
+ };
2364
+ };
2365
+ return {
2366
+ reduce: (reducer, initialState) => {
2367
+ let currentState = initialState;
2368
+ const listenerFn = createMatcher((msg) => {
2369
+ currentState = reducer(msg, currentState);
2370
+ if (isOnce) {
2371
+ stream._listeners.delete(listenerFn);
2372
+ }
2373
+ });
2374
+ const unsubscribe = () => {
2375
+ stream._listeners.delete(listenerFn);
2376
+ };
2377
+ stream._listeners.add(listenerFn);
2378
+ return { unsubscribe, getState: () => currentState };
2379
+ },
2380
+ tap: (effect) => {
2381
+ const listenerFn = createMatcher((msg) => {
2382
+ effect(msg);
2383
+ if (isOnce) {
2384
+ stream._listeners.delete(listenerFn);
2385
+ }
2386
+ });
2387
+ const unsubscribe = () => {
2388
+ stream._listeners.delete(listenerFn);
2389
+ };
2390
+ stream._listeners.add(listenerFn);
2391
+ return unsubscribe;
2392
+ }
2393
+ };
2394
+ };
2395
+ })(Stream || (Stream = {}));
2396
+
2290
2397
  // src/Core/TaskMaybe.ts
2291
2398
  var TaskMaybe;
2292
2399
  ((TaskMaybe2) => {
@@ -4350,6 +4457,7 @@ var Uniq;
4350
4457
  RetryPolicy,
4351
4458
  State,
4352
4459
  Str,
4460
+ Stream,
4353
4461
  Task,
4354
4462
  TaskMaybe,
4355
4463
  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-EPT3GYGN.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-IHLH2XYQ.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.49.0",
4
4
  "description": "Opinionated functional abstractions for TypeScript",
5
5
  "license": "BSD-3-Clause",
6
6
  "homepage": "https://pipelined.lozgachev.dev",
@@ -82,10 +82,10 @@
82
82
  },
83
83
  "size-limit": [
84
84
  { "path": "dist/index.js", "limit": "25 KB", "gzip": true },
85
- { "path": "dist/core.js", "limit": "15 KB", "gzip": true },
85
+ { "path": "dist/core.js", "limit": "16 KB", "gzip": true },
86
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 }
87
+ { "path": "dist/data.js", "limit": "13 KB", "gzip": true },
88
+ { "path": "dist/types.js", "limit": "2 KB", "gzip": true }
89
89
  ],
90
90
  "packageManager": "pnpm@11.6.0+sha512.9a36518224080c6fe5165afdcfe79bfa118c29be703f3f462b1e32efe1e98e47e8750b148e08286250aad4113cc7993ca413c4e2cd447752708c2ee5751bc95f"
91
91
  }