@soffinal/stream 0.2.2 → 0.2.4
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 +178 -81
- package/dist/index.js +2 -2
- package/dist/index.js.map +8 -8
- package/dist/reactive/state.d.ts +3 -3
- package/dist/reactive/state.d.ts.map +1 -1
- package/dist/stream.d.ts +52 -19
- package/dist/stream.d.ts.map +1 -1
- package/dist/transformers/filter.d.ts +74 -13
- package/dist/transformers/filter.d.ts.map +1 -1
- package/dist/transformers/flat.d.ts.map +1 -1
- package/dist/transformers/map.d.ts +83 -13
- package/dist/transformers/map.d.ts.map +1 -1
- package/dist/transformers/merge.d.ts +1 -3
- package/dist/transformers/merge.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/transformers/filter.md +244 -120
- package/src/transformers/map.md +336 -122
|
@@ -1,36 +1,106 @@
|
|
|
1
1
|
import { Stream } from "../stream.ts";
|
|
2
2
|
/**
|
|
3
3
|
* Adaptive map transformer that transforms values while maintaining state.
|
|
4
|
+
* Supports multiple concurrency strategies for async mappers.
|
|
4
5
|
*
|
|
5
6
|
* @template VALUE - The type of input values
|
|
6
7
|
* @template STATE - The type of the internal state object
|
|
7
8
|
* @template MAPPED - The type of output values after transformation
|
|
8
9
|
*
|
|
9
|
-
* @param
|
|
10
|
-
* @param
|
|
11
|
-
* - Must return `[transformedValue, newState]`
|
|
12
|
-
* - Can be async for complex transformations
|
|
13
|
-
* - Preserves order even with async operations
|
|
10
|
+
* @param initialStateOrMapper - Initial state object or mapper function
|
|
11
|
+
* @param statefulMapperOrOptions - Stateful mapper function or options for simple mappers
|
|
14
12
|
*
|
|
15
13
|
* @returns A transformer function that can be used with `.pipe()`
|
|
16
14
|
*
|
|
17
15
|
* @see {@link Stream} - Complete copy-paste transformers library
|
|
18
16
|
*
|
|
19
17
|
* @example
|
|
20
|
-
* // Simple transformation
|
|
21
|
-
* stream.pipe(map(
|
|
18
|
+
* // Simple synchronous transformation
|
|
19
|
+
* stream.pipe(map((value) => value * 2))
|
|
22
20
|
*
|
|
23
21
|
* @example
|
|
24
|
-
* //
|
|
22
|
+
* // Type transformation
|
|
23
|
+
* stream.pipe(map((value: number) => value.toString()))
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // Async transformation with sequential strategy (default)
|
|
25
27
|
* stream.pipe(
|
|
26
|
-
* map(
|
|
27
|
-
* const result = await
|
|
28
|
-
* return
|
|
28
|
+
* map(async (value) => {
|
|
29
|
+
* const result = await processAsync(value);
|
|
30
|
+
* return result;
|
|
29
31
|
* })
|
|
30
32
|
* )
|
|
31
33
|
*
|
|
32
|
-
|
|
34
|
+
* @example
|
|
35
|
+
* // Async transformation with concurrent-unordered strategy
|
|
36
|
+
* stream.pipe(
|
|
37
|
+
* map(async (value) => {
|
|
38
|
+
* const enriched = await enrichWithAPI(value);
|
|
39
|
+
* return enriched;
|
|
40
|
+
* }, { strategy: "concurrent-unordered" })
|
|
41
|
+
* )
|
|
33
42
|
*
|
|
43
|
+
* @example
|
|
44
|
+
* // Async transformation with concurrent-ordered strategy
|
|
45
|
+
* stream.pipe(
|
|
46
|
+
* map(async (value) => {
|
|
47
|
+
* const processed = await heavyProcessing(value);
|
|
48
|
+
* return processed;
|
|
49
|
+
* }, { strategy: "concurrent-ordered" })
|
|
50
|
+
* )
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* // Stateful transformation (always sequential)
|
|
54
|
+
* stream.pipe(
|
|
55
|
+
* map({ sum: 0 }, (state, value) => {
|
|
56
|
+
* const newSum = state.sum + value;
|
|
57
|
+
* return [{ value, runningSum: newSum }, { sum: newSum }];
|
|
58
|
+
* })
|
|
59
|
+
* )
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* // Complex stateful transformation
|
|
63
|
+
* stream.pipe(
|
|
64
|
+
* map({ count: 0, items: [] }, (state, value) => {
|
|
65
|
+
* const newItems = [...state.items, value];
|
|
66
|
+
* const newCount = state.count + 1;
|
|
67
|
+
* return [
|
|
68
|
+
* {
|
|
69
|
+
* item: value,
|
|
70
|
+
* index: newCount,
|
|
71
|
+
* total: newItems.length,
|
|
72
|
+
* history: newItems
|
|
73
|
+
* },
|
|
74
|
+
* { count: newCount, items: newItems }
|
|
75
|
+
* ];
|
|
76
|
+
* })
|
|
77
|
+
* )
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* // Async stateful transformation
|
|
81
|
+
* stream.pipe(
|
|
82
|
+
* map({ cache: new Map() }, async (state, value) => {
|
|
83
|
+
* const cached = state.cache.get(value);
|
|
84
|
+
* if (cached) return [cached, state];
|
|
85
|
+
*
|
|
86
|
+
* const processed = await expensiveOperation(value);
|
|
87
|
+
* const newCache = new Map(state.cache);
|
|
88
|
+
* newCache.set(value, processed);
|
|
89
|
+
*
|
|
90
|
+
* return [processed, { cache: newCache }];
|
|
91
|
+
* })
|
|
92
|
+
* )
|
|
34
93
|
*/
|
|
35
|
-
export declare
|
|
94
|
+
export declare const map: map.Map;
|
|
95
|
+
export declare namespace map {
|
|
96
|
+
type Options = {
|
|
97
|
+
strategy: "sequential" | "concurrent-unordered" | "concurrent-ordered";
|
|
98
|
+
};
|
|
99
|
+
type Mapper<VALUE = unknown, MAPPED = VALUE> = (value: VALUE) => MAPPED | Promise<MAPPED>;
|
|
100
|
+
type StatefulMapper<VALUE = unknown, STATE extends Record<string, unknown> = {}, MAPPED = VALUE> = (state: STATE, value: VALUE) => [MAPPED, STATE] | Promise<[MAPPED, STATE]>;
|
|
101
|
+
interface Map {
|
|
102
|
+
<VALUE, MAPPED>(mapper: Mapper<VALUE, MAPPED>, options?: Options): (stream: Stream<VALUE>) => Stream<MAPPED>;
|
|
103
|
+
<VALUE, STATE extends Record<string, unknown> = {}, MAPPED = VALUE>(initialState: STATE, mapper: StatefulMapper<VALUE, STATE, MAPPED>): (stream: Stream<VALUE>) => Stream<MAPPED>;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
36
106
|
//# sourceMappingURL=map.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map.d.ts","sourceRoot":"","sources":["../../src/transformers/map.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"map.d.ts","sourceRoot":"","sources":["../../src/transformers/map.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2FG;AACH,eAAO,MAAM,GAAG,EAAE,GAAG,CAAC,GAwFrB,CAAC;AAEF,yBAAiB,GAAG,CAAC;IACnB,KAAY,OAAO,GAAG;QAAE,QAAQ,EAAE,YAAY,GAAG,sBAAsB,GAAG,oBAAoB,CAAA;KAAE,CAAC;IACjG,KAAY,MAAM,CAAC,KAAK,GAAG,OAAO,EAAE,MAAM,GAAG,KAAK,IAAI,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjG,KAAY,cAAc,CAAC,KAAK,GAAG,OAAO,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,GAAG,KAAK,IAAI,CACxG,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,KAAK,KACT,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;IAEhD,UAAiB,GAAG;QAClB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC;QAC7G,CAAC,KAAK,EAAE,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,GAAG,KAAK,EAChE,YAAY,EAAE,KAAK,EACnB,MAAM,EAAE,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,GAC3C,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC;KAC9C;CACF"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Stream } from "../stream.ts";
|
|
2
|
-
type ValueOf<STREAM> = STREAM extends Stream<infer VALUE> ? VALUE : never;
|
|
3
2
|
/**
|
|
4
3
|
* Merge multiple streams into a single stream with temporal ordering.
|
|
5
4
|
*
|
|
@@ -30,6 +29,5 @@ type ValueOf<STREAM> = STREAM extends Stream<infer VALUE> ? VALUE : never;
|
|
|
30
29
|
*
|
|
31
30
|
|
|
32
31
|
*/
|
|
33
|
-
export declare function merge<VALUE, STREAMS extends [Stream<any>, ...Stream<any>[]]>(...streams: STREAMS): (stream: Stream<VALUE>) => Stream<VALUE | ValueOf<STREAMS[number]>>;
|
|
34
|
-
export {};
|
|
32
|
+
export declare function merge<VALUE, STREAMS extends [Stream<any>, ...Stream<any>[]]>(...streams: STREAMS): (stream: Stream<VALUE>) => Stream<VALUE | Stream.ValueOf<STREAMS[number]>>;
|
|
35
33
|
//# sourceMappingURL=merge.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../src/transformers/merge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC
|
|
1
|
+
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../src/transformers/merge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,OAAO,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,EAC1E,GAAG,OAAO,EAAE,OAAO,GAClB,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CA0B5E"}
|
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.
|
|
5
|
-
"description": "
|
|
4
|
+
"version": "0.2.4",
|
|
5
|
+
"description": "Type-safe event emitters that scale",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"devDependencies": {
|
|
8
8
|
"@types/bun": "latest"
|
|
@@ -1,202 +1,326 @@
|
|
|
1
1
|
# Filter Transformer
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
## Quick Start
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```typescript
|
|
8
|
+
import { Stream, filter } from "@soffinal/stream";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
const numbers = new Stream<number>();
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
filter(
|
|
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
|
-
|
|
19
|
+
## Basic Usage
|
|
16
20
|
|
|
17
|
-
###
|
|
21
|
+
### Synchronous Filtering
|
|
18
22
|
|
|
19
23
|
```typescript
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
44
|
+
// Type narrows to Stream<string>
|
|
45
|
+
const strings = mixed.pipe(filter((value): value is string => typeof value === "string"));
|
|
46
|
+
```
|
|
28
47
|
|
|
29
|
-
|
|
48
|
+
## Asynchronous Filtering
|
|
30
49
|
|
|
31
|
-
###
|
|
50
|
+
### Sequential Processing (Default)
|
|
32
51
|
|
|
33
52
|
```typescript
|
|
34
|
-
|
|
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
|
-
|
|
63
|
+
### Concurrent Strategies
|
|
64
|
+
|
|
65
|
+
For expensive async predicates, choose a concurrency strategy:
|
|
38
66
|
|
|
39
|
-
|
|
67
|
+
#### Concurrent Unordered
|
|
40
68
|
|
|
41
|
-
|
|
69
|
+
Results emit as soon as they complete, potentially out of order:
|
|
42
70
|
|
|
43
71
|
```typescript
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
87
|
+
#### Concurrent Ordered
|
|
49
88
|
|
|
50
|
-
|
|
89
|
+
Parallel processing but maintains original order:
|
|
51
90
|
|
|
52
91
|
```typescript
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
107
|
+
## Stateful Filtering
|
|
108
|
+
|
|
109
|
+
Maintain state across filter operations for complex logic:
|
|
110
|
+
|
|
111
|
+
### Basic Stateful Filtering
|
|
63
112
|
|
|
64
113
|
```typescript
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
### Level 4: Async
|
|
125
|
+
### Advanced State Management
|
|
77
126
|
|
|
78
127
|
```typescript
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
87
|
-
|
|
88
|
-
return [
|
|
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
|
-
|
|
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
|
-
##
|
|
176
|
+
## Stream Termination
|
|
96
177
|
|
|
97
|
-
|
|
178
|
+
Filters can terminate streams by returning `undefined`:
|
|
98
179
|
|
|
99
180
|
```typescript
|
|
100
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
190
|
+
stream.push("hello", "world", "STOP", "ignored");
|
|
191
|
+
// Only "hello" and "world" are emitted
|
|
192
|
+
```
|
|
113
193
|
|
|
114
|
-
###
|
|
194
|
+
### Conditional Termination
|
|
115
195
|
|
|
116
196
|
```typescript
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
235
|
+
## Error Handling
|
|
136
236
|
|
|
137
|
-
|
|
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
|
-
|
|
145
|
-
stream
|
|
146
|
-
|
|
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
|
-
##
|
|
254
|
+
## Common Patterns
|
|
150
255
|
|
|
151
|
-
|
|
256
|
+
### Throttling
|
|
152
257
|
|
|
153
258
|
```typescript
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
|
|
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
|
-
|
|
271
|
+
### Sampling
|
|
163
272
|
|
|
164
|
-
|
|
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
|
-
|
|
283
|
+
### Windowing
|
|
167
284
|
|
|
168
285
|
```typescript
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
297
|
+
## Type Signatures
|
|
192
298
|
|
|
193
|
-
|
|
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
|
-
##
|
|
318
|
+
## Best Practices
|
|
196
319
|
|
|
197
|
-
|
|
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
|
-
|
|
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.
|