@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 +58 -5
- package/dist/{chunk-UGVU2RTM.mjs → chunk-6N27QZFK.mjs} +1 -1
- package/dist/{chunk-LR63GW6J.mjs → chunk-U64ASY7P.mjs} +131 -0
- package/dist/core.d.mts +161 -1
- package/dist/core.d.ts +161 -1
- package/dist/core.js +132 -0
- package/dist/core.mjs +3 -1
- package/dist/data.mjs +2 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +132 -0
- package/dist/index.mjs +4 -2
- package/package.json +36 -16
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
|
|
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
|
|
25
|
-
entire toolkit is under
|
|
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. `
|
|
326
|
-
|
|
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
|
|
|
@@ -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-
|
|
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
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-
|
|
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-
|
|
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.
|
|
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": "
|
|
72
|
-
"@types/node": "
|
|
73
|
-
"@vitest/coverage-v8": "4.1.
|
|
74
|
-
"bumpp": "
|
|
75
|
-
"dprint": "0.
|
|
76
|
-
"fast-check": "4.
|
|
77
|
-
"oxlint": "1.
|
|
78
|
-
"size-limit": "
|
|
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.
|
|
81
|
+
"vitest": "4.1.10"
|
|
82
82
|
},
|
|
83
83
|
"size-limit": [
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
}
|