@soffinal/stream 0.2.4 → 0.3.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 +135 -637
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +10 -12
- package/dist/stream.d.ts +25 -6
- package/dist/stream.d.ts.map +1 -1
- package/dist/transformers/filter/filter.d.ts +18 -0
- package/dist/transformers/filter/filter.d.ts.map +1 -0
- package/dist/transformers/filter/index.d.ts +2 -0
- package/dist/transformers/filter/index.d.ts.map +1 -0
- package/dist/transformers/flat/flat.d.ts +3 -0
- package/dist/transformers/flat/flat.d.ts.map +1 -0
- package/dist/transformers/flat/index.d.ts +2 -0
- package/dist/transformers/flat/index.d.ts.map +1 -0
- package/dist/transformers/gate/gate.d.ts +43 -0
- package/dist/transformers/gate/gate.d.ts.map +1 -0
- package/dist/transformers/gate/index.d.ts +2 -0
- package/dist/transformers/gate/index.d.ts.map +1 -0
- package/dist/transformers/index.d.ts +6 -4
- package/dist/transformers/index.d.ts.map +1 -1
- package/dist/transformers/map/index.d.ts +2 -0
- package/dist/transformers/map/index.d.ts.map +1 -0
- package/dist/transformers/map/map.d.ts +14 -0
- package/dist/transformers/map/map.d.ts.map +1 -0
- package/dist/transformers/merge/index.d.ts +2 -0
- package/dist/transformers/merge/index.d.ts.map +1 -0
- package/dist/transformers/merge/merge.d.ts +3 -0
- package/dist/transformers/merge/merge.d.ts.map +1 -0
- package/dist/transformers/state/index.d.ts +2 -0
- package/dist/transformers/state/index.d.ts.map +1 -0
- package/dist/transformers/state/state.d.ts +35 -0
- package/dist/transformers/state/state.d.ts.map +1 -0
- package/package.json +15 -17
- package/dist/reactive/index.d.ts +0 -5
- package/dist/reactive/index.d.ts.map +0 -1
- package/dist/reactive/list.d.ts +0 -171
- package/dist/reactive/list.d.ts.map +0 -1
- package/dist/reactive/map.d.ts +0 -107
- package/dist/reactive/map.d.ts.map +0 -1
- package/dist/reactive/set.d.ts +0 -102
- package/dist/reactive/set.d.ts.map +0 -1
- package/dist/reactive/state.d.ts +0 -79
- package/dist/reactive/state.d.ts.map +0 -1
- package/dist/transformers/filter.d.ts +0 -96
- package/dist/transformers/filter.d.ts.map +0 -1
- package/dist/transformers/flat.d.ts +0 -31
- package/dist/transformers/flat.d.ts.map +0 -1
- package/dist/transformers/map.d.ts +0 -106
- package/dist/transformers/map.d.ts.map +0 -1
- package/dist/transformers/merge.d.ts +0 -33
- package/dist/transformers/merge.d.ts.map +0 -1
- package/src/transformers/filter.md +0 -326
- package/src/transformers/flat.md +0 -56
- package/src/transformers/map.md +0 -430
- package/src/transformers/merge.md +0 -79
package/README.md
CHANGED
|
@@ -3,45 +3,10 @@
|
|
|
3
3
|
[](https://badge.fury.io/js/@soffinal%2Fstream)
|
|
4
4
|
[](https://www.typescriptlang.org/)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Stream is like EventEmitter, but better. Send events to multiple listeners, transform data with `filter` and `map`, and never worry about memory leaks. Works with DOM elements, WebSockets, user interactions, or any async data source. Fully typed, zero dependencies, 5.5KB.
|
|
11
|
-
|
|
12
|
-
## Table of Contents
|
|
13
|
-
|
|
14
|
-
- [Features](#features)
|
|
15
|
-
- [Quick Start](#quick-start)
|
|
16
|
-
- [Installation](#installation)
|
|
17
|
-
- [Core Concepts](#core-concepts)
|
|
18
|
-
- [API Reference](#api-reference)
|
|
19
|
-
- [Examples](#examples)
|
|
20
|
-
- [Performance](#performance)
|
|
21
|
-
- [Migration Guide](#migration-guide)
|
|
22
|
-
- [Documentation](#documentation)
|
|
23
|
-
- [Contributing](#contributing)
|
|
24
|
-
- [License](#license)
|
|
25
|
-
|
|
26
|
-
## Features
|
|
27
|
-
|
|
28
|
-
- **Adaptive Constraints** - Transformers that learn and evolve based on stream history
|
|
29
|
-
- **Universal Primitives** - Four primitives: `filter`, `map`, `merge`, `flat`
|
|
30
|
-
- **Documentation-as-Distribution** - Copy-paste transformers embedded in JSDoc, no separate packages needed
|
|
31
|
-
- **Async-First** - Native async/await support with configurable concurrency control
|
|
32
|
-
- **Concurrency Strategies** - Sequential, concurrent-unordered, concurrent-ordered processing
|
|
33
|
-
- **Automatic Cleanup** - WeakRef-based listener cleanup prevents memory leaks
|
|
34
|
-
- **Multicast Streams** - One stream, unlimited consumers
|
|
35
|
-
- **Awaitable** - `await stream` for next value
|
|
36
|
-
- **Async Iterable** - Native `for await` loop support
|
|
37
|
-
- **Pipe Composition** - Stream-to-stream functional composition
|
|
38
|
-
- **Type Guards** - Built-in TypeScript type narrowing support
|
|
39
|
-
- **Reactive State** - Stateful values with automatic change propagation
|
|
40
|
-
- **Reactive Collections** - Lists, Maps, Sets with fine-grained events
|
|
41
|
-
- **Stream Termination** - Declarative stream lifecycle control
|
|
42
|
-
- **Zero Dependencies** - Lightweight and tree-shakeable
|
|
43
|
-
- **Universal** - Node.js, browsers, Deno, Bun, Cloudflare Workers
|
|
44
|
-
- **Full TypeScript** - Complete type safety without the burden
|
|
6
|
+
|
|
7
|
+
> **Composable reactive primitives for TypeScript**
|
|
8
|
+
|
|
9
|
+
Minimal reactive streaming library: type-safe events, stateful transformations, async iteration, automatic cleanup.
|
|
45
10
|
|
|
46
11
|
## Quick Start
|
|
47
12
|
|
|
@@ -50,704 +15,237 @@ import { Stream } from "@soffinal/stream";
|
|
|
50
15
|
|
|
51
16
|
const events = new Stream<string>();
|
|
52
17
|
|
|
53
|
-
events.listen(console.log);
|
|
18
|
+
events.listen((value) => console.log(value));
|
|
19
|
+
events.push("Hello", "World");
|
|
20
|
+
// Output: Hello, World
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
54
24
|
|
|
55
|
-
|
|
25
|
+
```bash
|
|
26
|
+
npm install @soffinal/stream
|
|
56
27
|
```
|
|
57
28
|
|
|
58
|
-
##
|
|
29
|
+
## Core Concepts
|
|
30
|
+
|
|
31
|
+
### 1. Stream - Push & Pull
|
|
59
32
|
|
|
60
33
|
```typescript
|
|
61
|
-
|
|
34
|
+
// Push: Emit values manually
|
|
35
|
+
const stream = new Stream<number>();
|
|
36
|
+
stream.listen((n) => console.log(n));
|
|
37
|
+
stream.push(1, 2, 3);
|
|
62
38
|
|
|
63
|
-
//
|
|
64
|
-
const
|
|
65
|
-
|
|
39
|
+
// Pull: Generate values from async source
|
|
40
|
+
const countdown = new Stream(async function* () {
|
|
41
|
+
for (let i = 5; i > 0; i--) {
|
|
42
|
+
yield i;
|
|
43
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
66
46
|
|
|
67
|
-
//
|
|
68
|
-
const
|
|
69
|
-
const ws = new WebSocket("ws://localhost
|
|
47
|
+
// Both: Push to a pull-based stream
|
|
48
|
+
const messages = new Stream(async function* () {
|
|
49
|
+
const ws = new WebSocket("ws://localhost");
|
|
70
50
|
while (ws.readyState === WebSocket.OPEN) {
|
|
71
|
-
yield await new Promise(
|
|
72
|
-
ws.onmessage =
|
|
51
|
+
yield await new Promise(resolve => {
|
|
52
|
+
ws.onmessage = e => resolve(e.data);
|
|
73
53
|
});
|
|
74
54
|
}
|
|
75
55
|
});
|
|
76
56
|
|
|
77
|
-
//
|
|
78
|
-
const processed = events
|
|
79
|
-
.pipe(filter((msg) => msg.length > 3)) // Simple filtering
|
|
80
|
-
.pipe(map((msg) => msg.toUpperCase())); // Transform to uppercase
|
|
81
|
-
|
|
82
|
-
// Async transformations with concurrency
|
|
83
|
-
const validated = events.pipe(
|
|
84
|
-
filter(
|
|
85
|
-
async (msg) => {
|
|
86
|
-
const isValid = await validateAsync(msg);
|
|
87
|
-
return isValid;
|
|
88
|
-
},
|
|
89
|
-
{ strategy: "concurrent-ordered" }
|
|
90
|
-
) // Parallel validation, ordered results
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
// Stateful transformers that learn and adapt
|
|
94
|
-
const runningAverage = numbers
|
|
95
|
-
.pipe(
|
|
96
|
-
filter({ count: 0 }, (state, value) => {
|
|
97
|
-
// Only pass every 3rd number, terminate after 10
|
|
98
|
-
if (state.count >= 10) return; // Stream termination
|
|
99
|
-
return [(state.count + 1) % 3 === 0, { count: state.count + 1 }];
|
|
100
|
-
})
|
|
101
|
-
)
|
|
102
|
-
.pipe(
|
|
103
|
-
map({ sum: 0, count: 0 }, (state, value) => {
|
|
104
|
-
const newSum = state.sum + value;
|
|
105
|
-
const newCount = state.count + 1;
|
|
106
|
-
const average = newSum / newCount;
|
|
107
|
-
return [
|
|
108
|
-
{ value, average },
|
|
109
|
-
{ sum: newSum, count: newCount },
|
|
110
|
-
];
|
|
111
|
-
})
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
// Automatic cleanup with DOM elements
|
|
115
|
-
const element = document.createElement('div');
|
|
116
|
-
events.listen(value => {
|
|
117
|
-
element.textContent = value;
|
|
118
|
-
}, element); // Auto-removed when element is GC'd
|
|
119
|
-
|
|
120
|
-
// Copy-paste transformers from JSDoc
|
|
121
|
-
const limited = numbers.pipe(take(5)); // Limit to 5 items
|
|
122
|
-
const indexed = events.pipe(withIndex()); // Add indices
|
|
123
|
-
const delayed = processed.pipe(delay(100)); // Delay each value
|
|
124
|
-
|
|
125
|
-
// Multiple consumers
|
|
126
|
-
processed.listen((msg) => console.log("Processed:", msg));
|
|
127
|
-
validated.listen((msg) => console.log("Validated:", msg));
|
|
128
|
-
runningAverage.listen(({ value, average }) => console.log(`Value: ${value}, Running Average: ${average}`));
|
|
129
|
-
|
|
130
|
-
// Reactive state
|
|
131
|
-
const counter = new State(0);
|
|
132
|
-
counter.listen((count) => (document.title = `Count: ${count}`));
|
|
133
|
-
counter.value++; // UI updates automatically
|
|
57
|
+
messages.push("manual event"); // Can still push!
|
|
134
58
|
```
|
|
135
59
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
### Package Managers
|
|
139
|
-
|
|
140
|
-
```bash
|
|
141
|
-
# npm
|
|
142
|
-
npm install @soffinal/stream
|
|
60
|
+
### 2. Automatic Cleanup
|
|
143
61
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
# pnpm
|
|
148
|
-
pnpm add @soffinal/stream
|
|
149
|
-
|
|
150
|
-
# bun
|
|
151
|
-
bun add @soffinal/stream
|
|
152
|
-
|
|
153
|
-
# Deno
|
|
154
|
-
deno add jsr:@soffinal/stream
|
|
155
|
-
```
|
|
62
|
+
```typescript
|
|
63
|
+
// Disposable pattern
|
|
64
|
+
using stream.listen(handler);
|
|
156
65
|
|
|
157
|
-
|
|
66
|
+
// Manual cleanup
|
|
67
|
+
const cleanup = stream.listen(handler);
|
|
68
|
+
cleanup();
|
|
158
69
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
<script type="module">
|
|
162
|
-
import { Stream, State } from "https://cdn.jsdelivr.net/npm/@soffinal/stream@latest/dist/index.js";
|
|
163
|
-
</script>
|
|
70
|
+
// AbortSignal
|
|
71
|
+
stream.listen(handler, controller.signal);
|
|
164
72
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
</script>
|
|
73
|
+
// Stream as signal
|
|
74
|
+
const stop = new Stream<void>();
|
|
75
|
+
stream.listen(handler, stop);
|
|
76
|
+
stop.push(); // Remove listener
|
|
170
77
|
```
|
|
171
78
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
### Streams: Multicast Event Pipelines
|
|
175
|
-
|
|
176
|
-
A `Stream` is a multicast, async iterable that pushes values to multiple listeners while being awaitable for the next value.
|
|
79
|
+
### 3. Transform with pipe
|
|
177
80
|
|
|
178
81
|
```typescript
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
// Multiple consumers automatically share the same data
|
|
182
|
-
userEvents.listen((event) => analytics.track(event));
|
|
183
|
-
userEvents.listen((event) => notifications.send(event));
|
|
184
|
-
userEvents.listen((event) => database.save(event));
|
|
185
|
-
|
|
186
|
-
// Await the next event
|
|
187
|
-
const nextEvent = await userEvents;
|
|
188
|
-
|
|
189
|
-
// Async iteration
|
|
190
|
-
for await (const event of userEvents) {
|
|
191
|
-
if (event.type === "critical") break;
|
|
192
|
-
processEvent(event);
|
|
193
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### Automatic Listener Cleanup
|
|
82
|
+
const numbers = new Stream<number>();
|
|
197
83
|
|
|
198
|
-
|
|
84
|
+
const doubled = numbers.pipe(filter((n) => n > 0)).pipe(map((n) => n * 2));
|
|
199
85
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
// 1. Manual cleanup
|
|
204
|
-
const cleanup = stream.listen(value => console.log(value));
|
|
205
|
-
cleanup(); // Remove listener
|
|
206
|
-
|
|
207
|
-
// 2. AbortSignal cleanup
|
|
208
|
-
const controller = new AbortController();
|
|
209
|
-
stream.listen(value => console.log(value), controller.signal);
|
|
210
|
-
controller.abort(); // Remove listener
|
|
211
|
-
|
|
212
|
-
// 3. WeakRef automatic cleanup (NEW!)
|
|
213
|
-
const element = document.createElement('div');
|
|
214
|
-
stream.listen(value => {
|
|
215
|
-
element.textContent = value;
|
|
216
|
-
}, element);
|
|
217
|
-
// Listener automatically removed when element is garbage collected
|
|
218
|
-
// Perfect for DOM elements, components, and temporary objects
|
|
86
|
+
doubled.listen((n) => console.log(n));
|
|
87
|
+
numbers.push(-1, 2, 3);
|
|
88
|
+
// Output: 4, 6
|
|
219
89
|
```
|
|
220
90
|
|
|
221
|
-
|
|
222
|
-
- Zero memory leaks with DOM elements
|
|
223
|
-
- No manual cleanup needed
|
|
224
|
-
- Works with any object (components, instances, etc.)
|
|
225
|
-
- Leverages JavaScript's garbage collector
|
|
226
|
-
- Ideal for UI frameworks (React, Vue, Svelte, etc.)
|
|
91
|
+
### 4. Async Iteration
|
|
227
92
|
|
|
228
93
|
```typescript
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const dataStream = new Stream<Data>();
|
|
233
|
-
|
|
234
|
-
// Auto-cleanup when component unmounts
|
|
235
|
-
dataStream.listen(data => {
|
|
236
|
-
element.innerHTML = renderTemplate(data);
|
|
237
|
-
}, element);
|
|
238
|
-
|
|
239
|
-
return element;
|
|
94
|
+
for await (const value of stream) {
|
|
95
|
+
console.log(value);
|
|
96
|
+
if (done) break;
|
|
240
97
|
}
|
|
241
|
-
|
|
242
|
-
// When element is removed from DOM and GC'd, listener is automatically cleaned up
|
|
243
98
|
```
|
|
244
99
|
|
|
245
|
-
|
|
100
|
+
## Transformers
|
|
246
101
|
|
|
247
|
-
|
|
102
|
+
### state - Reactive State
|
|
248
103
|
|
|
249
104
|
```typescript
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
// Infinite chaining - every pipe returns a Stream
|
|
256
|
-
const result = stream
|
|
257
|
-
.pipe(filter((v) => v > 0))
|
|
258
|
-
.pipe(map((v) => v * 2))
|
|
259
|
-
.pipe(take(5))
|
|
260
|
-
.pipe(delay(100))
|
|
261
|
-
.pipe(distinct()); // Always chainable
|
|
105
|
+
const counter = new Stream<number>().pipe(state(0));
|
|
106
|
+
|
|
107
|
+
counter.listen((n) => console.log(n));
|
|
108
|
+
counter.state.value = 5; // Triggers listener
|
|
109
|
+
console.log(counter.state.value); // 5
|
|
262
110
|
```
|
|
263
111
|
|
|
264
|
-
**
|
|
112
|
+
**[📖 Full Documentation →](src/transformers/state/state.md)**
|
|
265
113
|
|
|
266
|
-
|
|
114
|
+
### gate - Flow Control
|
|
267
115
|
|
|
268
116
|
```typescript
|
|
269
|
-
const
|
|
117
|
+
const stream = new Stream<number>().pipe(gate());
|
|
270
118
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
119
|
+
stream.listen((n) => console.log(n));
|
|
120
|
+
stream.push(1); // Logs: 1
|
|
121
|
+
stream.gate.close();
|
|
122
|
+
stream.push(2); // Blocked
|
|
123
|
+
stream.gate.open();
|
|
124
|
+
stream.push(3); // Logs: 3
|
|
275
125
|
```
|
|
276
126
|
|
|
277
|
-
|
|
127
|
+
**[📖 Full Documentation →](src/transformers/gate/gate.md)**
|
|
278
128
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
#### 1. Filter
|
|
129
|
+
### filter - Remove Values
|
|
282
130
|
|
|
283
131
|
```typescript
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
// Simple filtering
|
|
287
|
-
stream.pipe(filter((value) => value > 0));
|
|
288
|
-
|
|
289
|
-
// Type guard filtering
|
|
290
|
-
stream.pipe(filter((value): value is number => typeof value === "number"));
|
|
132
|
+
// Simple
|
|
133
|
+
stream.pipe(filter((n) => n > 0));
|
|
291
134
|
|
|
292
|
-
// Async
|
|
293
|
-
stream.pipe(
|
|
294
|
-
filter(
|
|
295
|
-
async (value) => {
|
|
296
|
-
const isValid = await validateAsync(value);
|
|
297
|
-
return isValid;
|
|
298
|
-
},
|
|
299
|
-
{ strategy: "concurrent-ordered" }
|
|
300
|
-
) // Parallel validation, ordered results
|
|
301
|
-
);
|
|
135
|
+
// Async
|
|
136
|
+
stream.pipe(filter(async (n) => await validate(n)));
|
|
302
137
|
|
|
303
|
-
// Stateful
|
|
138
|
+
// Stateful
|
|
304
139
|
stream.pipe(
|
|
305
|
-
filter({ count: 0 }, (state,
|
|
306
|
-
if (state.count >= 10) return; //
|
|
307
|
-
return [
|
|
308
|
-
})
|
|
140
|
+
filter({ count: 0 }, (state, n) => {
|
|
141
|
+
if (state.count >= 10) return; // Stop after 10
|
|
142
|
+
return [n > 0, { count: state.count + 1 }];
|
|
143
|
+
}),
|
|
309
144
|
);
|
|
310
145
|
```
|
|
311
146
|
|
|
312
|
-
**[📖
|
|
147
|
+
**[📖 Full Documentation →](src/transformers/filter/filter.md)**
|
|
313
148
|
|
|
314
|
-
|
|
149
|
+
### map - Transform Values
|
|
315
150
|
|
|
316
151
|
```typescript
|
|
317
|
-
|
|
152
|
+
// Simple
|
|
153
|
+
stream.pipe(map((n) => n * 2));
|
|
318
154
|
|
|
319
|
-
//
|
|
320
|
-
stream.pipe(map((
|
|
155
|
+
// Async
|
|
156
|
+
stream.pipe(map(async (n) => await process(n)));
|
|
321
157
|
|
|
322
|
-
//
|
|
323
|
-
stream.pipe(map((value: number) => value.toString()));
|
|
324
|
-
|
|
325
|
-
// Async transformation with concurrency strategies
|
|
158
|
+
// Stateful
|
|
326
159
|
stream.pipe(
|
|
327
|
-
map(
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
},
|
|
332
|
-
{ strategy: "concurrent-unordered" }
|
|
333
|
-
) // Parallel processing, results as completed
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
// Stateful transformation with context
|
|
337
|
-
stream.pipe(
|
|
338
|
-
map({ sum: 0 }, (state, value) => {
|
|
339
|
-
const newSum = state.sum + value;
|
|
340
|
-
return [{ value, runningSum: newSum }, { sum: newSum }];
|
|
341
|
-
})
|
|
160
|
+
map({ sum: 0 }, (state, n) => {
|
|
161
|
+
const newSum = state.sum + n;
|
|
162
|
+
return [newSum, { sum: newSum }];
|
|
163
|
+
}),
|
|
342
164
|
);
|
|
343
165
|
```
|
|
344
166
|
|
|
345
|
-
**[📖
|
|
167
|
+
**[📖 Full Documentation →](src/transformers/map/map.md)**
|
|
346
168
|
|
|
347
|
-
|
|
169
|
+
### merge - Combine Streams
|
|
348
170
|
|
|
349
171
|
```typescript
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const stream1 = new Stream<number>();
|
|
353
|
-
const stream2 = new Stream<string>();
|
|
172
|
+
const numbers = new Stream<number>();
|
|
173
|
+
const strings = new Stream<string>();
|
|
354
174
|
|
|
355
|
-
|
|
356
|
-
const combined = stream1.pipe(merge(stream2));
|
|
175
|
+
const combined = numbers.pipe(merge(strings));
|
|
357
176
|
// Type: Stream<number | string>
|
|
358
|
-
|
|
359
|
-
combined.listen((value) => {
|
|
360
|
-
if (typeof value === "number") {
|
|
361
|
-
console.log("Number:", value);
|
|
362
|
-
} else {
|
|
363
|
-
console.log("String:", value);
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
177
|
```
|
|
367
178
|
|
|
368
|
-
**[📖
|
|
179
|
+
**[📖 Full Documentation →](src/transformers/merge/merge.md)**
|
|
369
180
|
|
|
370
|
-
|
|
181
|
+
### flat - Flatten Arrays
|
|
371
182
|
|
|
372
183
|
```typescript
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
// Transform 1 array event → N individual events
|
|
376
|
-
const arrayStream = new Stream<number[]>();
|
|
377
|
-
const individualNumbers = arrayStream.pipe(flat());
|
|
184
|
+
const arrays = new Stream<number[]>();
|
|
185
|
+
const numbers = arrays.pipe(flat());
|
|
378
186
|
|
|
379
|
-
|
|
380
|
-
// Type: Stream<number>
|
|
381
|
-
|
|
382
|
-
// Configurable depth flattening
|
|
383
|
-
const deepArrays = new Stream<number[][][]>();
|
|
384
|
-
const flattened = deepArrays.pipe(flat(2)); // Flatten 2 levels deep
|
|
385
|
-
// Type: Stream<number>
|
|
187
|
+
arrays.push([1, 2, 3]); // Emits: 1, 2, 3
|
|
386
188
|
```
|
|
387
189
|
|
|
388
|
-
**[📖
|
|
389
|
-
|
|
390
|
-
### Documentation-as-Distribution: Copy-Paste Transformers
|
|
391
|
-
|
|
392
|
-
No separate repos, no CLI tools, no package management - just copy-paste ready transformers embedded in JSDoc!
|
|
393
|
-
|
|
394
|
-
#### The Educational Transparency
|
|
395
|
-
|
|
396
|
-
Our approach makes **every implementation pattern visible and learnable**:
|
|
397
|
-
|
|
398
|
-
```typescript
|
|
399
|
-
// 📦 All transformers are copy-pastable from IntelliSense!
|
|
400
|
-
// Hover over 'Stream' to see the complete transformers library
|
|
401
|
-
|
|
402
|
-
// Example: Users don't just get functions - they get implementation education
|
|
403
|
-
const searchInput = new Stream<string>(); // ← Hover here for full library
|
|
404
|
-
const searchResults = searchInput
|
|
405
|
-
.pipe(distinct()) // Copy from Stream JSDoc - learn deduplication patterns
|
|
406
|
-
.pipe(take(10)) // Copy from Stream JSDoc - learn termination patterns
|
|
407
|
-
.pipe(delay(300)) // Copy from Stream JSDoc - learn async transformation
|
|
408
|
-
.pipe(simpleMap((query) => searchAPI(query))); // Copy from Stream JSDoc - learn mapping patterns
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
#### What Users Actually Learn
|
|
412
|
-
|
|
413
|
-
When users hover over any function in JSDoc, they see **complete implementation patterns**:
|
|
414
|
-
|
|
415
|
-
```typescript
|
|
416
|
-
// Users see EXACTLY how to build transformers
|
|
417
|
-
const take = <T>(n: number) =>
|
|
418
|
-
filter<T, { count: number }>({ count: 0 }, (state, value) => {
|
|
419
|
-
if (state.count >= n) return; // ← Learn termination patterns
|
|
420
|
-
return [true, { count: state.count + 1 }]; // ← Learn state evolution
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
const distinct = <T>() =>
|
|
424
|
-
filter<T, { seen: Set<T> }>({ seen: new Set() }, (state, value) => {
|
|
425
|
-
if (state.seen.has(value)) return [false, state]; // ← Learn deduplication logic
|
|
426
|
-
state.seen.add(value); // ← Learn state mutation patterns
|
|
427
|
-
return [true, state];
|
|
428
|
-
});
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
#### From Consumers to Creators
|
|
432
|
-
|
|
433
|
-
This transparency empowers users to become **transformer architects**:
|
|
434
|
-
|
|
435
|
-
```typescript
|
|
436
|
-
// After learning from JSDoc examples, users create their own:
|
|
437
|
-
const withTimestamp = <T>() =>
|
|
438
|
-
map<T, {}, { value: T; timestamp: number }>(
|
|
439
|
-
{}, // ← Learned: empty state when no memory needed
|
|
440
|
-
(_, value) => [
|
|
441
|
-
{ value, timestamp: Date.now() }, // ← Learned: transformation pattern
|
|
442
|
-
{}, // ← Learned: state management
|
|
443
|
-
]
|
|
444
|
-
);
|
|
445
|
-
|
|
446
|
-
const rateLimited = <T>(maxPerSecond: number) =>
|
|
447
|
-
filter<T, { timestamps: number[] }>({ timestamps: [] }, (state, value) => {
|
|
448
|
-
const now = Date.now();
|
|
449
|
-
const recent = state.timestamps.filter((t) => now - t < 1000);
|
|
450
|
-
if (recent.length >= maxPerSecond) return [false, { timestamps: recent }];
|
|
451
|
-
return [true, { timestamps: [...recent, now] }];
|
|
452
|
-
});
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
#### Benefits Beyond Bundle Size
|
|
456
|
-
|
|
457
|
-
- ✅ **Zero friction** - Copy-paste ready transformers
|
|
458
|
-
- ✅ **Perfect discoverability** - IntelliSense shows all available transformers
|
|
459
|
-
- ✅ **Always up-to-date** - Examples match current API version
|
|
460
|
-
- ✅ **No ecosystem fragmentation** - Everything in one place
|
|
461
|
-
- ✅ **Educational transparency** - Users learn implementation patterns
|
|
462
|
-
- ✅ **Infinite extensibility** - Users become transformer creators
|
|
463
|
-
- ✅ **Self-documenting** - Usage examples included with working code
|
|
464
|
-
- ✅ **Zero bundle cost** - JSDoc stripped at compile time
|
|
190
|
+
**[📖 Full Documentation →](src/transformers/flat/flat.md)**
|
|
465
191
|
|
|
466
|
-
|
|
192
|
+
## Write Your Own
|
|
467
193
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
1. **User discovers** transformer in JSDoc
|
|
471
|
-
2. **User learns** implementation pattern
|
|
472
|
-
3. **User creates** custom transformers for their domain
|
|
473
|
-
4. **User shares** patterns with their team
|
|
474
|
-
5. **Team creates** hundreds of variations
|
|
475
|
-
6. **Knowledge multiplies** exponentially across the community
|
|
476
|
-
|
|
477
|
-
**How it works:**
|
|
478
|
-
|
|
479
|
-
1. Hover over `Stream` in your IDE to see the complete transformers library
|
|
480
|
-
2. Or hover over individual functions for quick references
|
|
481
|
-
3. Copy the transformer you need
|
|
482
|
-
4. Use immediately - perfect TypeScript inference included!
|
|
483
|
-
5. **Learn the patterns** and create your own infinite variations
|
|
484
|
-
|
|
485
|
-
**Available Transformers (via JSDoc):**
|
|
486
|
-
|
|
487
|
-
- `take(n)`, `skip(n)`, `distinct()`, `tap(fn)` - Essential filtering patterns
|
|
488
|
-
- `withIndex()`, `delay(ms)`, `pluck(key)`, `scan(fn, initial)` - Common transformation patterns
|
|
489
|
-
- `toState(initialValue)` - Convert streams to reactive state
|
|
490
|
-
- More transformers added with each release!
|
|
491
|
-
|
|
492
|
-
**📊 Bundle Size Impact:**
|
|
493
|
-
|
|
494
|
-
- **Package size**: Currently ~15KB, grows with JSDoc transformer examples over time
|
|
495
|
-
- **Your app bundle**: Always only 5.5KB (runtime code only, zero JSDoc overhead)
|
|
496
|
-
- **Tree-shaking**: Only imported functions included in final bundle
|
|
497
|
-
- **JSDoc transformers**: "Free" - rich transformer library without production cost
|
|
498
|
-
|
|
499
|
-
**You're not just building applications - you're learning a paradigm that scales infinitely.**
|
|
500
|
-
|
|
501
|
-
### Manual Composition
|
|
194
|
+
A transformer is just a function:
|
|
502
195
|
|
|
503
196
|
```typescript
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
if (state.taken >= count) return; // Terminate after N items
|
|
508
|
-
return [true, { taken: state.taken + 1 }];
|
|
197
|
+
const double = (stream: Stream<number>) =>
|
|
198
|
+
new Stream(async function* () {
|
|
199
|
+
for await (const n of stream) yield n * 2;
|
|
509
200
|
});
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
### Reactive State: Stateful Values
|
|
513
|
-
|
|
514
|
-
`State` extends `Stream` with a current value that can be read and written:
|
|
515
|
-
|
|
516
|
-
```typescript
|
|
517
|
-
const user = new State<User | null>(null);
|
|
518
|
-
const theme = new State<"light" | "dark">("light");
|
|
519
|
-
const counter = new State(0);
|
|
520
|
-
|
|
521
|
-
// Read current value
|
|
522
|
-
console.log(counter.value); // 0
|
|
523
|
-
|
|
524
|
-
// Write triggers all listeners
|
|
525
|
-
counter.value = 5;
|
|
526
|
-
|
|
527
|
-
// State from transformed streams
|
|
528
|
-
const source = new Stream<number>();
|
|
529
|
-
const derivedState = new State(0, source.pipe(map((v) => v * 2)));
|
|
530
|
-
|
|
531
|
-
// Derived state using transformers
|
|
532
|
-
const isLoggedIn = user.pipe(map((u) => u !== null));
|
|
533
|
-
|
|
534
|
-
const userDisplayName = user.pipe(
|
|
535
|
-
filter((u) => u !== null),
|
|
536
|
-
map((u) => `${u.firstName} ${u.lastName}`)
|
|
537
|
-
);
|
|
538
|
-
|
|
539
|
-
// Convert streams to state with toState transformer
|
|
540
|
-
const processedState = source
|
|
541
|
-
.pipe(filter((v) => v > 0))
|
|
542
|
-
.pipe(map((v) => v.toString()))
|
|
543
|
-
.pipe(toState("0")); // Explicit initial value
|
|
544
|
-
|
|
545
|
-
// Automatic UI updates
|
|
546
|
-
isLoggedIn.listen((loggedIn) => {
|
|
547
|
-
document.body.classList.toggle("authenticated", loggedIn);
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
// State changes propagate through the pipeline
|
|
551
|
-
user.value = { firstName: "John", lastName: "Doe" };
|
|
552
|
-
// Triggers: isLoggedIn → true, userDisplayName → 'John Doe'
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
### Reactive Collections: Fine-Grained Change Events
|
|
556
|
-
|
|
557
|
-
Collections that emit specific change events for efficient UI updates:
|
|
558
|
-
|
|
559
|
-
```typescript
|
|
560
|
-
import { List, Map, Set } from "@soffinal/stream";
|
|
561
|
-
|
|
562
|
-
const todos = new List<Todo>();
|
|
563
|
-
const userCache = new Map<string, User>();
|
|
564
|
-
const activeUsers = new Set<string>();
|
|
565
|
-
|
|
566
|
-
// React to specific operations
|
|
567
|
-
todos.insert.listen(([index, todo]) => {
|
|
568
|
-
console.log(`Todo inserted at ${index}:`, todo);
|
|
569
|
-
renderTodoAtIndex(index, todo);
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
todos.delete.listen(([index, todo]) => {
|
|
573
|
-
console.log(`Todo removed from ${index}:`, todo);
|
|
574
|
-
removeTodoFromDOM(index);
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
// Map changes
|
|
578
|
-
userCache.set.listen(([key, user]) => {
|
|
579
|
-
console.log(`User cached: ${key}`, user);
|
|
580
|
-
updateUserInUI(key, user);
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
// Set changes
|
|
584
|
-
activeUsers.add.listen((userId) => {
|
|
585
|
-
console.log(`User ${userId} came online`);
|
|
586
|
-
showOnlineIndicator(userId);
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
activeUsers.delete.listen((userId) => {
|
|
590
|
-
console.log(`User ${userId} went offline`);
|
|
591
|
-
hideOnlineIndicator(userId);
|
|
592
|
-
});
|
|
593
201
|
|
|
594
|
-
|
|
595
|
-
todos.push({ id: 1, text: "Learn streams", done: false });
|
|
596
|
-
userCache.set("user1", { name: "Alice", email: "alice@example.com" });
|
|
597
|
-
activeUsers.add("user1");
|
|
202
|
+
stream.pipe(double);
|
|
598
203
|
```
|
|
599
204
|
|
|
600
|
-
## API
|
|
205
|
+
## API
|
|
601
206
|
|
|
602
207
|
### Stream\<T>
|
|
603
208
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
- `
|
|
607
|
-
- `
|
|
608
|
-
- `pipe<OUTPUT extends Stream<any>>(transformer: (stream: this) => OUTPUT): OUTPUT` - Apply any transformer
|
|
609
|
-
- `withContext(context: object): AsyncIterator<T>` - Async iterator bound to context lifetime
|
|
610
|
-
|
|
611
|
-
#### Async Interface
|
|
612
|
-
|
|
613
|
-
- `then<U>(callback?: (value: T) => U): Promise<U>` - Promise interface for next value
|
|
614
|
-
- `[Symbol.asyncIterator](): AsyncIterator<T>` - Async iteration support
|
|
615
|
-
|
|
616
|
-
#### Properties
|
|
617
|
-
|
|
618
|
-
- `hasListeners: boolean` - Whether stream has active listeners
|
|
619
|
-
- `listenerAdded: Stream<void>` - Emits when listener is added
|
|
620
|
-
- `listenerRemoved: Stream<void>` - Emits when listener is removed
|
|
621
|
-
|
|
622
|
-
### State\<T> extends Stream\<T>
|
|
623
|
-
|
|
624
|
-
#### Constructor
|
|
625
|
-
|
|
626
|
-
- `new State(initialValue: T)` - Create state with initial value
|
|
627
|
-
- `new State(initialValue: T, stream: Stream<T>)` - Create state from stream
|
|
628
|
-
|
|
629
|
-
#### Additional Properties
|
|
630
|
-
|
|
631
|
-
- `value: T` - Current state value (get/set)
|
|
632
|
-
|
|
633
|
-
### Universal Transformers
|
|
634
|
-
|
|
635
|
-
#### filter(predicate, options?)
|
|
636
|
-
|
|
637
|
-
- **Simple**: `filter((value) => boolean)`
|
|
638
|
-
- **Type Guard**: `filter((value): value is Type => boolean)` (sync only)
|
|
639
|
-
- **Async**: `filter(async (value) => boolean, { strategy? })` with concurrency options
|
|
640
|
-
- **Stateful**: `filter(state, (state, value) => [boolean, newState])` (always sequential)
|
|
641
|
-
- **Termination**: Return `undefined` to terminate stream
|
|
642
|
-
- **Strategies**: `"sequential"` | `"concurrent-unordered"` | `"concurrent-ordered"`
|
|
643
|
-
|
|
644
|
-
#### map(mapper, options?)
|
|
645
|
-
|
|
646
|
-
- **Simple**: `map((value) => newValue)`
|
|
647
|
-
- **Async**: `map(async (value) => newValue, { strategy? })` with concurrency options
|
|
648
|
-
- **Stateful**: `map(state, (state, value) => [newValue, newState])` (always sequential)
|
|
649
|
-
- **Strategies**: `"sequential"` | `"concurrent-unordered"` | `"concurrent-ordered"`
|
|
650
|
-
|
|
651
|
-
#### merge(...streams)
|
|
652
|
-
|
|
653
|
-
- **Basic**: `stream.pipe(merge(stream2, stream3))`
|
|
654
|
-
- **Type-Safe**: Automatically creates union types
|
|
655
|
-
- **Temporal Order**: Maintains chronological sequence
|
|
656
|
-
|
|
657
|
-
#### flat(depth?)
|
|
658
|
-
|
|
659
|
-
- **Basic**: `stream.pipe(flat())` - Flatten one level
|
|
660
|
-
- **Deep**: `stream.pipe(flat(2))` - Flatten N levels
|
|
661
|
-
- **Event Multiplication**: 1 array event → N individual events
|
|
209
|
+
- `push(...values: T[])` - Emit values
|
|
210
|
+
- `listen(callback, context?)` - Add listener
|
|
211
|
+
- `pipe(transformer)` - Transform stream
|
|
212
|
+
- `clear()` - Remove all listeners
|
|
662
213
|
|
|
663
|
-
###
|
|
214
|
+
### Async
|
|
664
215
|
|
|
665
|
-
|
|
216
|
+
- `await stream` - Wait for next value
|
|
217
|
+
- `for await (const value of stream)` - Iterate
|
|
666
218
|
|
|
667
|
-
|
|
668
|
-
- `delete: Stream<[number, T]>` - Deletion events
|
|
669
|
-
- `clear: Stream<void>` - Clear events
|
|
219
|
+
## Philosophy
|
|
670
220
|
|
|
671
|
-
|
|
221
|
+
**2 primitives**: `Stream` + `pipe`
|
|
672
222
|
|
|
673
|
-
|
|
674
|
-
- `delete: Stream<[K, V]>` - Delete events
|
|
675
|
-
- `clear: Stream<void>` - Clear events
|
|
223
|
+
**6 transformers**: `state`, `gate`, `filter`, `map`, `merge`, `flat`
|
|
676
224
|
|
|
677
|
-
|
|
225
|
+
Everything else you compose yourself.
|
|
678
226
|
|
|
679
|
-
|
|
680
|
-
- `delete: Stream<T>` - Delete events
|
|
681
|
-
- `clear: Stream<void>` - Clear events
|
|
682
|
-
|
|
683
|
-
## Performance
|
|
684
|
-
|
|
685
|
-
### Bundle Size
|
|
686
|
-
|
|
687
|
-
- **Runtime bundle** - 5.5KB minified, 1.6KB gzipped
|
|
688
|
-
- **Package size** - Starts small, grows with JSDoc transformer library
|
|
689
|
-
- **Your production app** - Always gets only the 5.5KB runtime code
|
|
690
|
-
- **Tree-shakeable** - Import only what you use
|
|
691
|
-
|
|
692
|
-
### Benchmarks
|
|
693
|
-
|
|
694
|
-
- **Fast startup** - Zero dependencies, instant initialization
|
|
695
|
-
- **Efficient pipelines** - Optimized transformer composition
|
|
696
|
-
- **Memory bounded** - Built-in backpressure handling
|
|
697
|
-
- **Automatic cleanup** - WeakRef prevents memory leaks
|
|
698
|
-
|
|
699
|
-
## Runtime Support
|
|
700
|
-
|
|
701
|
-
- **Modern browsers** supporting ES2020+ (WeakRef support)
|
|
702
|
-
- **Node.js** 16+
|
|
703
|
-
- **Deno** 1.0+
|
|
704
|
-
- **Bun** 1.0+
|
|
705
|
-
- **Cloudflare Workers**
|
|
706
|
-
|
|
707
|
-
## Migration Guide
|
|
708
|
-
|
|
709
|
-
### From EventEmitter
|
|
227
|
+
**Efficient by design**: Transformers execute once per value. Multiple listeners share the same computation:
|
|
710
228
|
|
|
711
229
|
```typescript
|
|
712
|
-
|
|
713
|
-
import { EventEmitter } from "events";
|
|
714
|
-
const emitter = new EventEmitter();
|
|
715
|
-
emitter.on("data", console.log);
|
|
716
|
-
emitter.emit("data", "hello");
|
|
230
|
+
const expensive = source.pipe(map(async v => await heavyComputation(v)));
|
|
717
231
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
stream.push("hello");
|
|
232
|
+
expensive.listen(v => updateUI(v));
|
|
233
|
+
expensive.listen(v => logToAnalytics(v));
|
|
234
|
+
expensive.listen(v => saveToCache(v));
|
|
235
|
+
// heavyComputation() runs ONCE per value, not 3 times
|
|
723
236
|
```
|
|
724
237
|
|
|
725
|
-
##
|
|
726
|
-
|
|
727
|
-
### Transformer Guides
|
|
728
|
-
|
|
729
|
-
- **[Filter Transformer →](src/transformers/filter.md)** - Concurrency strategies, type guards, stateful filtering, and stream termination
|
|
730
|
-
- **[Map Transformer →](src/transformers/map.md)** - Concurrency strategies, type transformations, stateful mapping, and performance optimization
|
|
731
|
-
- **[Merge Transformer →](src/transformers/merge.md)** - Stream orchestration and type-safe combination
|
|
732
|
-
- **[Flat Transformer →](src/transformers/flat.md)** - Event multiplication and array flattening
|
|
238
|
+
## Performance
|
|
733
239
|
|
|
734
|
-
|
|
240
|
+
- 1.6KB gzipped
|
|
241
|
+
- Zero dependencies
|
|
242
|
+
- Automatic memory management
|
|
243
|
+
- Tree-shakeable
|
|
735
244
|
|
|
736
|
-
|
|
245
|
+
## Browser Support
|
|
737
246
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
```bash
|
|
741
|
-
git clone https://github.com/soffinal/stream.git
|
|
742
|
-
cd stream
|
|
743
|
-
bun install
|
|
744
|
-
bun test
|
|
745
|
-
```
|
|
247
|
+
Chrome 84+, Firefox 79+, Safari 14.1+, Node.js 16+, Deno, Bun
|
|
746
248
|
|
|
747
249
|
## License
|
|
748
250
|
|
|
749
251
|
MIT © [Soffinal](https://github.com/soffinal)
|
|
750
|
-
|
|
751
|
-
Contact: <smari.sofiane@gmail.com>
|
|
752
|
-
|
|
753
|
-
---
|