@soffinal/stream 0.2.1 → 0.2.3

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/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@soffinal/stream",
3
3
  "module": "./dist/index.js",
4
- "version": "0.2.1",
5
- "description": "A reactive event streaming library for TypeScript/JavaScript",
4
+ "version": "0.2.3",
5
+ "description": "Multicast Event Pipelines with functional composition for TypeScript/JavaScript",
6
6
  "type": "module",
7
7
  "devDependencies": {
8
8
  "@types/bun": "latest"
@@ -1,202 +1,326 @@
1
1
  # Filter Transformer
2
2
 
3
- ## The Adaptive Gatekeeper
3
+ The `filter` transformer selectively passes values through a stream based on predicate functions. It supports synchronous and asynchronous filtering, type guards, stateful operations, and multiple concurrency strategies.
4
4
 
5
- Traditional filtering is binary and stateless - a value either passes or doesn't. But real-world filtering often requires **memory, learning, and evolution**. The `filter` transformer embodies **Adaptive Reactive Programming** - where the gatekeeper remembers, and can even decide when to stop.
5
+ ## Quick Start
6
6
 
7
- ## Design
7
+ ```typescript
8
+ import { Stream, filter } from "@soffinal/stream";
8
9
 
9
- ### Why State-First Architecture?
10
+ const numbers = new Stream<number>();
10
11
 
11
- ```typescript
12
- filter(initialState, (state, value) => [boolean, newState]);
12
+ // Simple filtering
13
+ const positives = numbers.pipe(filter((n) => n > 0));
14
+
15
+ positives.listen(console.log);
16
+ numbers.push(-1, 2, -3, 4); // Outputs: 2, 4
13
17
  ```
14
18
 
15
- **State comes first** because it's the foundation of adaptation. This isn't just filtering - it's **Adaptive gatekeeping** that evolves with each event.
19
+ ## Basic Usage
16
20
 
17
- ### The Dual Return Pattern
21
+ ### Synchronous Filtering
18
22
 
19
23
  ```typescript
20
- return [shouldPass, newState]; // Continue with evolution
21
- return; // Terminate with wisdom
24
+ // Basic predicate
25
+ stream.pipe(filter((value) => value > 10));
26
+
27
+ // Complex conditions
28
+ stream.pipe(filter((user) => user.active && user.age >= 18));
29
+
30
+ // Null/undefined filtering
31
+ stream.pipe(filter((value) => value != null));
22
32
  ```
23
33
 
24
- **Two outcomes, infinite possibilities:**
34
+ ### Type Guards
35
+
36
+ Filter supports TypeScript type guards for type narrowing:
37
+
38
+ ```typescript
39
+ const mixed = new Stream<string | number>();
40
+
41
+ // Type narrows to Stream<number>
42
+ const numbers = mixed.pipe(filter((value): value is number => typeof value === "number"));
25
43
 
26
- - `[boolean, state]` - The filter learns and continues
27
- - `void` - The filter decides the stream has served its purpose
44
+ // Type narrows to Stream<string>
45
+ const strings = mixed.pipe(filter((value): value is string => typeof value === "string"));
46
+ ```
28
47
 
29
- This mirrors human decision-making: we either let something through (and remember why), or we decide we've seen enough.
48
+ ## Asynchronous Filtering
30
49
 
31
- ### Argument Order
50
+ ### Sequential Processing (Default)
32
51
 
33
52
  ```typescript
34
- (state, value) => // State first, value second
53
+ const stream = new Stream<string>();
54
+
55
+ const validated = stream.pipe(
56
+ filter(async (email) => {
57
+ const isValid = await validateEmail(email);
58
+ return isValid;
59
+ })
60
+ );
35
61
  ```
36
62
 
37
- **State precedes value** because context shapes perception. We don't judge events in isolation - we judge them based on what we've learned. The state is the accumulated transformations; the value is just the current moment.
63
+ ### Concurrent Strategies
64
+
65
+ For expensive async predicates, choose a concurrency strategy:
38
66
 
39
- ## The Adaptive Constraint System
67
+ #### Concurrent Unordered
40
68
 
41
- ### Level 1: Simple Gatekeeping
69
+ Results emit as soon as they complete, potentially out of order:
42
70
 
43
71
  ```typescript
44
- // Traditional filtering - no memory, no learning
45
- stream.pipe(filter({}, (_, value) => [value > 0, {}]));
72
+ const stream = new Stream<string>();
73
+
74
+ const validated = stream.pipe(
75
+ filter(
76
+ async (url) => {
77
+ const isReachable = await checkURL(url);
78
+ return isReachable;
79
+ },
80
+ { strategy: "concurrent-unordered" }
81
+ )
82
+ );
83
+
84
+ // URLs may emit in different order based on response times
46
85
  ```
47
86
 
48
- Even "simple" filtering uses the adaptive architecture. The empty state `{}` represents a gatekeeper that doesn't need memory - but could develop it.
87
+ #### Concurrent Ordered
49
88
 
50
- ### Level 2: Memory-Based Filtering
89
+ Parallel processing but maintains original order:
51
90
 
52
91
  ```typescript
53
- // The gatekeeper remembers and counts
54
- stream.pipe(
55
- filter({ count: 0 }, (state, value) => {
56
- const newCount = state.count + 1;
57
- return [newCount % 3 === 0, { count: newCount }]; // Every 3rd passes
58
- })
92
+ const stream = new Stream<User>();
93
+
94
+ const verified = stream.pipe(
95
+ filter(
96
+ async (user) => {
97
+ const isVerified = await verifyUser(user.id);
98
+ return isVerified;
99
+ },
100
+ { strategy: "concurrent-ordered" }
101
+ )
59
102
  );
103
+
104
+ // Users always emit in original order despite varying verification times
60
105
  ```
61
106
 
62
- ### Level 3: Termination
107
+ ## Stateful Filtering
108
+
109
+ Maintain state across filter operations for complex logic:
110
+
111
+ ### Basic Stateful Filtering
63
112
 
64
113
  ```typescript
65
- // The gatekeeper knows when enough is enough
66
- stream.pipe(
67
- filter({ seen: 0 }, (state, value) => {
68
- if (state.seen >= 10) return; // Wisdom: we've seen enough
69
- return [value > 0, { seen: state.seen + 1 }];
114
+ const stream = new Stream<number>();
115
+
116
+ // Take only first 5 items
117
+ const limited = stream.pipe(
118
+ filter({ count: 0 }, (state, value) => {
119
+ if (state.count >= 5) return; // Terminate stream
120
+ return [true, { count: state.count + 1 }];
70
121
  })
71
122
  );
72
123
  ```
73
124
 
74
- **Stream termination** represents the ultimate adaptive behavior - knowing when to stop. This isn't just filtering; it's **stream lifecycle management**.
75
-
76
- ### Level 4: Async
125
+ ### Advanced State Management
77
126
 
78
127
  ```typescript
79
- // The gatekeeper consults external validation
80
- stream.pipe(
81
- filter({ cache: new Map() }, async (state, value) => {
82
- if (state.cache.has(value)) {
83
- return [state.cache.get(value), state]; // Remember previous decisions
128
+ const stream = new Stream<string>();
129
+
130
+ // Deduplicate values
131
+ const unique = stream.pipe(
132
+ filter({ seen: new Set<string>() }, (state, value) => {
133
+ if (state.seen.has(value)) {
134
+ return [false, state]; // Skip duplicate
84
135
  }
85
136
 
86
- const isValid = await validateAsync(value);
87
- state.cache.set(value, isValid); // Learn for next time
88
- return [isValid, state];
137
+ const newSeen = new Set(state.seen);
138
+ newSeen.add(value);
139
+ return [true, { seen: newSeen }];
89
140
  })
90
141
  );
91
142
  ```
92
143
 
93
- **Async filtering with memory** - the gatekeeper doesn't just validate, it **builds institutional knowledge**.
144
+ ### Complex Stateful Logic
145
+
146
+ ```typescript
147
+ const events = new Stream<{ type: string; timestamp: number }>();
148
+
149
+ // Rate limiting: max 5 events per second
150
+ const rateLimited = events.pipe(
151
+ filter(
152
+ {
153
+ timestamps: [] as number[],
154
+ maxPerSecond: 5,
155
+ },
156
+ (state, event) => {
157
+ const now = event.timestamp;
158
+ const recent = state.timestamps.filter((t) => now - t < 1000);
159
+
160
+ if (recent.length >= state.maxPerSecond) {
161
+ return [false, { ...state, timestamps: recent }];
162
+ }
163
+
164
+ return [
165
+ true,
166
+ {
167
+ ...state,
168
+ timestamps: [...recent, now],
169
+ },
170
+ ];
171
+ }
172
+ )
173
+ );
174
+ ```
94
175
 
95
- ## Essential Copy-Paste Transformers
176
+ ## Stream Termination
96
177
 
97
- ### simpleFilter - Gateway to Adaptation
178
+ Filters can terminate streams by returning `undefined`:
98
179
 
99
180
  ```typescript
100
- // For users transitioning from traditional filtering
101
- const simpleFilter = <T>(predicate: (value: T) => boolean | Promise<boolean>) =>
102
- filter<T, {}>({}, async (_, value) => {
103
- const shouldPass = await predicate(value);
104
- return [shouldPass, {}];
105
- });
181
+ const stream = new Stream<string>();
106
182
 
107
- // Usage: familiar syntax, adaptive foundation
108
- stream.pipe(simpleFilter((x) => x > 0));
109
- stream.pipe(simpleFilter(async (user) => await isValid(user)));
110
- ```
183
+ const untilStop = stream.pipe(
184
+ filter((value) => {
185
+ if (value === "STOP") return; // Terminates stream
186
+ return value.length > 0;
187
+ })
188
+ );
111
189
 
112
- **Design choice**: `simpleFilter` is a **bridge**, not a replacement. It introduces users to the adaptive architecture while providing familiar syntax. The empty state `{}` is an invitation to evolution.
190
+ stream.push("hello", "world", "STOP", "ignored");
191
+ // Only "hello" and "world" are emitted
192
+ ```
113
193
 
114
- ### take - The Counting Gatekeeper
194
+ ### Conditional Termination
115
195
 
116
196
  ```typescript
117
- const take = <T>(n: number) =>
118
- filter<T, { count: number }>({ count: 0 }, (state, value) => {
119
- if (state.count >= n) return; // Wisdom: we have enough
120
- return [true, { count: state.count + 1 }];
121
- });
197
+ const numbers = new Stream<number>();
198
+
199
+ const untilNegative = numbers.pipe(
200
+ filter({ sum: 0 }, (state, value) => {
201
+ const newSum = state.sum + value;
202
+ if (newSum < 0) return; // Terminate when sum goes negative
203
+
204
+ return [value > 0, { sum: newSum }];
205
+ })
206
+ );
122
207
  ```
123
208
 
124
- ### distinct - The Memory Gatekeeper
209
+ ## Performance Considerations
210
+
211
+ ### When to Use Concurrency
212
+
213
+ - **Sequential**: Default choice, maintains order, lowest overhead
214
+ - **Concurrent-unordered**: Use when order doesn't matter and predicates are expensive
215
+ - **Concurrent-ordered**: Use when order matters but predicates are expensive
216
+
217
+ ### Memory Management
218
+
219
+ Stateful filters maintain state objects. For large datasets:
125
220
 
126
221
  ```typescript
127
- const distinct = <T>() =>
128
- filter<T, { seen: Set<T> }>({ seen: new Set() }, (state, value) => {
129
- if (state.seen.has(value)) return [false, state];
130
- state.seen.add(value);
131
- return [true, state];
132
- });
222
+ // Good: Bounded state
223
+ filter({ count: 0, limit: 1000 }, (state, value) => {
224
+ if (state.count >= state.limit) return;
225
+ return [predicate(value), { ...state, count: state.count + 1 }];
226
+ });
227
+
228
+ // Avoid: Unbounded state growth
229
+ filter({ history: [] }, (state, value) => {
230
+ // This grows indefinitely!
231
+ return [true, { history: [...state.history, value] }];
232
+ });
133
233
  ```
134
234
 
135
- ### tap - The Observer Gatekeeper
235
+ ## Error Handling
136
236
 
137
- ```typescript
138
- const tap = <T>(fn: (value: T) => void | Promise<void>) =>
139
- filter<T, {}>({}, async (_, value) => {
140
- await fn(value);
141
- return [true, {}]; // Always pass through
142
- });
237
+ Errors in predicates will propagate and potentially terminate the stream:
143
238
 
144
- // Usage: Side effects without changing the stream
145
- stream.pipe(tap((value) => console.log("Saw:", value)));
146
- stream.pipe(tap(async (value) => await logToDatabase(value)));
239
+ ```typescript
240
+ const stream = new Stream<number>();
241
+
242
+ const safe = stream.pipe(
243
+ filter((value) => {
244
+ try {
245
+ return riskyPredicate(value);
246
+ } catch (error) {
247
+ console.error("Filter error:", error);
248
+ return false; // Skip problematic values
249
+ }
250
+ })
251
+ );
147
252
  ```
148
253
 
149
- ## The Termination
254
+ ## Common Patterns
150
255
 
151
- Stream termination isn't failure - it's **purposeful completion**. When a filter returns `void`, it's saying: "I have served my purpose, and this stream's journey ends here."
256
+ ### Throttling
152
257
 
153
258
  ```typescript
154
- // A filter that knows its mission
155
- const untilCondition = <T>(condition: (value: T) => boolean) =>
156
- filter<T, {}>({}, (_, value) => {
157
- if (condition(value)) return; // Mission complete
158
- return [true, {}];
259
+ const throttle = <T>(ms: number) =>
260
+ filter<T, { lastEmit: number }>({ lastEmit: 0 }, (state, value) => {
261
+ const now = Date.now();
262
+ if (now - state.lastEmit < ms) {
263
+ return [false, state];
264
+ }
265
+ return [true, { lastEmit: now }];
159
266
  });
267
+
268
+ stream.pipe(throttle(1000)); // Max one value per second
160
269
  ```
161
270
 
162
- This represents a fundamental shift from infinite streams to **purpose-driven streams** that know when their work is done.
271
+ ### Sampling
163
272
 
164
- ## Enhanced Pipe Integration
273
+ ```typescript
274
+ const sample = <T>(n: number) =>
275
+ filter<T, { count: number }>({ count: 0 }, (state, value) => {
276
+ const shouldEmit = (state.count + 1) % n === 0;
277
+ return [shouldEmit, { count: state.count + 1 }];
278
+ });
279
+
280
+ stream.pipe(sample(3)); // Every 3rd value
281
+ ```
165
282
 
166
- The new pipe architecture enables seamless integration:
283
+ ### Windowing
167
284
 
168
285
  ```typescript
169
- // Filter integrates with any transformer
170
- const result = stream
171
- .pipe(filter({}, (_, v) => [v > 0, {}])) // Returns Stream<T>
172
- .pipe(map({}, (_, v) => [v.toString(), {}])) // Returns Stream<string>
173
- .pipe(toState("0")); // Returns State<string>
286
+ const slidingWindow = <T>(size: number) =>
287
+ filter<T, { window: T[] }>({ window: [] }, (state, value) => {
288
+ const newWindow = [...state.window, value].slice(-size);
289
+ const shouldEmit = newWindow.length === size;
174
290
 
175
- // Complex filtering chains
176
- const processed = source
177
- .pipe(
178
- filter({ seen: 0 }, (state, v) => {
179
- if (state.seen >= 100) return; // Terminate after 100
180
- return [v > 0, { seen: state.seen + 1 }];
181
- })
182
- )
183
- .pipe(tap((v) => console.log("Positive:", v)))
184
- .pipe(
185
- filter({ count: 0 }, (state, v) => {
186
- return [state.count % 2 === 0, { count: state.count + 1 }]; // Every other
187
- })
188
- );
291
+ return [shouldEmit, { window: newWindow }];
292
+ });
293
+
294
+ stream.pipe(slidingWindow(5)); // Emit when window is full
189
295
  ```
190
296
 
191
- **Note**: Filters compose naturally because they all speak the same language - **adaptive constraints** that can terminate, remember, and evolve.
297
+ ## Type Signatures
192
298
 
193
- **Design insight**: Filtering State creates **conditional reactivity** - the derived state only reacts to values that pass the adaptive constraints.
299
+ ```typescript
300
+ // Simple predicate with optional concurrency
301
+ filter<VALUE>(
302
+ predicate: (value: VALUE) => boolean | void | Promise<boolean | void>,
303
+ options?: { strategy: "sequential" | "concurrent-unordered" | "concurrent-ordered" }
304
+ ): (stream: Stream<VALUE>) => Stream<VALUE>
305
+
306
+ // Type guard predicate (synchronous only)
307
+ filter<VALUE, FILTERED extends VALUE>(
308
+ predicate: (value: VALUE) => value is FILTERED
309
+ ): (stream: Stream<VALUE>) => Stream<FILTERED>
310
+
311
+ // Stateful predicate (always sequential)
312
+ filter<VALUE, STATE>(
313
+ initialState: STATE,
314
+ predicate: (state: STATE, value: VALUE) => [boolean, STATE] | void
315
+ ): (stream: Stream<VALUE>) => Stream<VALUE>
316
+ ```
194
317
 
195
- ## Conclusion
318
+ ## Best Practices
196
319
 
197
- The `filter` transformer isn't just about removing unwanted values - it's about **intelligent gatekeeping** that:
320
+ 1. **Choose the right strategy**: Use sequential for simple predicates, concurrent for expensive async operations
321
+ 2. **Manage state size**: Keep stateful filter state bounded to prevent memory leaks
322
+ 3. **Handle errors gracefully**: Wrap risky predicates in try-catch blocks
323
+ 4. **Use type guards**: Leverage TypeScript's type narrowing for better type safety
324
+ 5. **Consider termination**: Use `return undefined` to cleanly terminate streams when conditions are met
198
325
 
199
- - **Remembers** previous decisions (state)
200
- - **Learns** from patterns (adaptation)
201
- - **Evolves** behavior over time (constraints)
202
- - **Knows** when to stop (termination)
326
+ The filter transformer is a powerful tool for stream processing that scales from simple synchronous predicates to complex stateful async operations with optimal performance characteristics.