@soffinal/stream 0.1.4 → 0.2.1
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/CHANGELOG.md +22 -0
- package/README.md +439 -298
- package/dist/index.d.ts +2 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +12 -8
- package/dist/reactive/index.d.ts +5 -0
- package/dist/reactive/index.d.ts.map +1 -0
- package/dist/{list.d.ts → reactive/list.d.ts} +19 -1
- package/dist/reactive/list.d.ts.map +1 -0
- package/dist/{map.d.ts → reactive/map.d.ts} +11 -1
- package/dist/reactive/map.d.ts.map +1 -0
- package/dist/{set.d.ts → reactive/set.d.ts} +11 -1
- package/dist/reactive/set.d.ts.map +1 -0
- package/dist/{state.d.ts → reactive/state.d.ts} +18 -18
- package/dist/reactive/state.d.ts.map +1 -0
- package/dist/stream.d.ts +94 -289
- package/dist/stream.d.ts.map +1 -1
- package/dist/transformers/filter.d.ts +35 -0
- package/dist/transformers/filter.d.ts.map +1 -0
- package/dist/transformers/flat.d.ts +31 -0
- package/dist/transformers/flat.d.ts.map +1 -0
- package/dist/transformers/index.d.ts +5 -0
- package/dist/transformers/index.d.ts.map +1 -0
- package/dist/transformers/map.d.ts +36 -0
- package/dist/transformers/map.d.ts.map +1 -0
- package/dist/transformers/merge.d.ts +35 -0
- package/dist/transformers/merge.d.ts.map +1 -0
- package/package.json +5 -8
- package/src/transformers/filter.md +202 -0
- package/src/transformers/flat.md +56 -0
- package/src/transformers/map.md +216 -0
- package/src/transformers/merge.md +79 -0
- package/dist/benchmark.d.ts +0 -16
- package/dist/benchmark.d.ts.map +0 -1
- package/dist/list.d.ts.map +0 -1
- package/dist/map.d.ts.map +0 -1
- package/dist/set.d.ts.map +0 -1
- package/dist/state.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://bundlephobia.com/package/@soffinal/stream)
|
|
7
7
|
|
|
8
|
-
> **
|
|
8
|
+
> **A reactive streaming library with Adaptive Constraints**
|
|
9
9
|
|
|
10
|
-
A
|
|
10
|
+
A groundbreaking streaming library that introduces **Adaptive Reactive Programming** - where transformers maintain state and evolve their behavior based on stream history. Built with four universal primitives that compose into infinite possibilities.
|
|
11
11
|
|
|
12
12
|
## Table of Contents
|
|
13
13
|
|
|
@@ -18,48 +18,74 @@ A modern, async-first streaming library that treats asynchronous data flow as a
|
|
|
18
18
|
- [API Reference](#api-reference)
|
|
19
19
|
- [Examples](#examples)
|
|
20
20
|
- [Performance](#performance)
|
|
21
|
-
- [Browser Support](#browser-support)
|
|
22
21
|
- [Migration Guide](#migration-guide)
|
|
22
|
+
- [Documentation](#documentation)
|
|
23
23
|
- [Contributing](#contributing)
|
|
24
24
|
- [License](#license)
|
|
25
25
|
|
|
26
26
|
## Features
|
|
27
27
|
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
- 📦 **Zero Dependencies** - Lightweight
|
|
28
|
+
- 🧠 **Adaptive Constraints** - Transformers that learn and evolve based on stream history
|
|
29
|
+
- 🔧 **Universal Primitives** - Four algebraic primitives: `filter`, `map`, `merge`, `flat`
|
|
30
|
+
- 📚 **Documentation-as-Distribution** - Copy-paste transformers embedded in JSDoc, no separate packages needed
|
|
31
|
+
- ⚡ **Async-First** - All operations support async with order preservation
|
|
32
|
+
- 🔄 **Multicast Streams** - One stream, unlimited consumers
|
|
33
|
+
- ⏳ **Awaitable** - `await stream` for next value
|
|
34
|
+
- 🔁 **Async Iterable** - Native `for await` loop support
|
|
35
|
+
- 🛠️ **Pipe Composition** - Functional transformer composition
|
|
36
|
+
- 📊 **Reactive State** - Stateful values with automatic change propagation
|
|
37
|
+
- 📋 **Reactive Collections** - Lists, Maps, Sets with fine-grained events
|
|
38
|
+
- 🗑️ **Stream Termination** - Declarative stream lifecycle control
|
|
39
|
+
- 📦 **Zero Dependencies** - Lightweight and tree-shakeable
|
|
40
40
|
- 🌐 **Universal** - Node.js, browsers, Deno, Bun, Cloudflare Workers
|
|
41
|
-
- 📘 **Full TypeScript** - Complete type safety
|
|
41
|
+
- 📘 **Full TypeScript** - Complete type safety without the burden
|
|
42
42
|
|
|
43
43
|
## Quick Start
|
|
44
44
|
|
|
45
45
|
```typescript
|
|
46
|
-
import { Stream, State } from "@soffinal/stream";
|
|
46
|
+
import { Stream, State, filter, map, merge } from "@soffinal/stream";
|
|
47
47
|
|
|
48
|
-
// Create reactive
|
|
48
|
+
// Create reactive streams
|
|
49
49
|
const events = new Stream<string>();
|
|
50
|
-
const
|
|
50
|
+
const numbers = new Stream<number>();
|
|
51
51
|
|
|
52
|
-
//
|
|
53
|
-
const processed = events
|
|
52
|
+
// Pipe-based transformation with Adaptive Constraints
|
|
53
|
+
const processed = events
|
|
54
|
+
.pipe(simpleFilter((msg) => msg.length > 3)) // Simple filtering
|
|
55
|
+
.pipe(simpleMap((msg) => msg.toUpperCase())); // Transform to uppercase
|
|
54
56
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
// Stateful transformers that learn and adapt
|
|
58
|
+
const runningAverage = numbers
|
|
59
|
+
.pipe(
|
|
60
|
+
filter({ count: 0 }, (state, value) => {
|
|
61
|
+
// Only pass every 3rd number, terminate after 10
|
|
62
|
+
if (state.count >= 10) return; // Stream termination
|
|
63
|
+
return [(state.count + 1) % 3 === 0, { count: state.count + 1 }];
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
.pipe(
|
|
67
|
+
map({ sum: 0, count: 0 }, (state, value) => {
|
|
68
|
+
const newSum = state.sum + value;
|
|
69
|
+
const newCount = state.count + 1;
|
|
70
|
+
const average = newSum / newCount;
|
|
71
|
+
return [
|
|
72
|
+
{ value, average },
|
|
73
|
+
{ sum: newSum, count: newCount },
|
|
74
|
+
];
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Copy-paste transformers from JSDoc
|
|
79
|
+
const limited = numbers.pipe(take(5)); // Limit to 5 items
|
|
80
|
+
const indexed = events.pipe(withIndex()); // Add indices
|
|
81
|
+
const delayed = processed.pipe(delay(100)); // Delay each value
|
|
58
82
|
|
|
59
|
-
//
|
|
60
|
-
|
|
83
|
+
// Multiple consumers
|
|
84
|
+
processed.listen((msg) => console.log("Processed:", msg));
|
|
85
|
+
runningAverage.listen(({ value, average }) => console.log(`Value: ${value}, Running Average: ${average}`));
|
|
61
86
|
|
|
62
|
-
// Reactive state
|
|
87
|
+
// Reactive state
|
|
88
|
+
const counter = new State(0);
|
|
63
89
|
counter.listen((count) => (document.title = `Count: ${count}`));
|
|
64
90
|
counter.value++; // UI updates automatically
|
|
65
91
|
```
|
|
@@ -102,234 +128,442 @@ deno add jsr:@soffinal/stream
|
|
|
102
128
|
|
|
103
129
|
## Core Concepts
|
|
104
130
|
|
|
105
|
-
### Streams:
|
|
131
|
+
### Streams: Multicast Event Pipelines
|
|
106
132
|
|
|
107
|
-
A `Stream` is
|
|
133
|
+
A `Stream` is a multicast, async iterable that pushes values to multiple listeners while being awaitable for the next value.
|
|
108
134
|
|
|
109
135
|
```typescript
|
|
110
136
|
const userEvents = new Stream<UserEvent>();
|
|
111
137
|
|
|
112
|
-
// Multiple consumers
|
|
138
|
+
// Multiple consumers automatically share the same data
|
|
113
139
|
userEvents.listen((event) => analytics.track(event));
|
|
114
140
|
userEvents.listen((event) => notifications.send(event));
|
|
115
141
|
userEvents.listen((event) => database.save(event));
|
|
116
142
|
|
|
117
|
-
//
|
|
143
|
+
// Await the next event
|
|
118
144
|
const nextEvent = await userEvents;
|
|
145
|
+
|
|
146
|
+
// Async iteration
|
|
147
|
+
for await (const event of userEvents) {
|
|
148
|
+
if (event.type === "critical") break;
|
|
149
|
+
processEvent(event);
|
|
150
|
+
}
|
|
119
151
|
```
|
|
120
152
|
|
|
121
|
-
###
|
|
153
|
+
### Pipe: Stream-to-Stream Composition
|
|
122
154
|
|
|
123
|
-
|
|
155
|
+
The `pipe` method enforces reactive composition - it only accepts functions that return Stream instances, maintaining the infinite reactive pipeline:
|
|
124
156
|
|
|
125
157
|
```typescript
|
|
126
|
-
|
|
158
|
+
// All transformers return Streams - infinite chaining
|
|
159
|
+
stream.pipe(filter({}, (_, v) => [v > 0, {}])); // → Stream<T>
|
|
160
|
+
stream.pipe(map({}, (_, v) => [v.toString(), {}])); // → Stream<string>
|
|
161
|
+
stream.pipe(toState("initial")); // → State<string> (extends Stream)
|
|
127
162
|
|
|
128
|
-
//
|
|
163
|
+
// Infinite chaining - every pipe returns a Stream
|
|
129
164
|
const result = stream
|
|
130
|
-
.filter((
|
|
131
|
-
.map((
|
|
132
|
-
.
|
|
165
|
+
.pipe(filter({}, (_, v) => [v > 0, {}]))
|
|
166
|
+
.pipe(map({}, (_, v) => [v * 2, {}]))
|
|
167
|
+
.pipe(take(5))
|
|
168
|
+
.pipe(delay(100))
|
|
169
|
+
.pipe(distinct()); // Always chainable
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Streams are infinite** - Like event emitters, they don't terminate naturally. The `pipe` constraint ensures you maintain the reactive paradigm throughout your entire pipeline.
|
|
173
|
+
|
|
174
|
+
**Perfect TypeScript inference** - no annotations needed:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
const numbers = new Stream<number>();
|
|
178
|
+
|
|
179
|
+
// TypeScript knows these are all Streams
|
|
180
|
+
const doubled = numbers.pipe(map({}, (_, n) => [n * 2, {}])); // Stream<number>
|
|
181
|
+
const strings = numbers.pipe(map({}, (_, n) => [n.toString(), {}])); // Stream<string>
|
|
182
|
+
const state = numbers.pipe(toState(0)); // State<number>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Universal Primitives: The Four Algebraic Operations
|
|
186
|
+
|
|
187
|
+
All stream operations are built from four universal primitives with **Adaptive Constraints**:
|
|
133
188
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
189
|
+
#### 1. Filter - Adaptive Gatekeeper
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import { filter } from "@soffinal/stream";
|
|
193
|
+
|
|
194
|
+
// Simple filtering
|
|
195
|
+
stream.pipe(filter({}, (_, value) => [value > 0, {}]));
|
|
196
|
+
|
|
197
|
+
// Stateful filtering with termination
|
|
198
|
+
stream.pipe(
|
|
199
|
+
filter({ count: 0 }, (state, value) => {
|
|
200
|
+
if (state.count >= 10) return; // Terminate after 10 items
|
|
201
|
+
return [value > 0, { count: state.count + 1 }];
|
|
202
|
+
})
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Async filtering
|
|
206
|
+
stream.pipe(
|
|
207
|
+
filter({}, async (_, value) => {
|
|
137
208
|
const isValid = await validateAsync(value);
|
|
138
|
-
return isValid;
|
|
209
|
+
return [isValid, {}];
|
|
139
210
|
})
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
211
|
+
);
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**[📖 Complete Filter Documentation →](src/transformers/filter.md)**
|
|
144
215
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
216
|
+
#### 2. Map - Adaptive Transformer
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import { map } from "@soffinal/stream";
|
|
220
|
+
|
|
221
|
+
// Simple transformation
|
|
222
|
+
stream.pipe(map({}, (_, value) => [value * 2, {}]));
|
|
223
|
+
|
|
224
|
+
// Stateful transformation with context
|
|
225
|
+
stream.pipe(
|
|
226
|
+
map({ sum: 0 }, (state, value) => {
|
|
227
|
+
const newSum = state.sum + value;
|
|
228
|
+
return [{ value, runningSum: newSum }, { sum: newSum }];
|
|
152
229
|
})
|
|
153
|
-
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// Async transformation with order preservation
|
|
233
|
+
stream.pipe(
|
|
234
|
+
map({}, async (_, value) => {
|
|
235
|
+
const enriched = await enrichWithAPI(value);
|
|
236
|
+
return [{ original: value, enriched }, {}];
|
|
237
|
+
})
|
|
238
|
+
);
|
|
154
239
|
```
|
|
155
240
|
|
|
156
|
-
|
|
241
|
+
**[📖 Complete Map Documentation →](src/transformers/map.md)**
|
|
242
|
+
|
|
243
|
+
#### 3. Merge - Stream Orchestration
|
|
157
244
|
|
|
158
245
|
```typescript
|
|
159
|
-
import {
|
|
246
|
+
import { merge } from "@soffinal/stream";
|
|
160
247
|
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
.pipe(filter(0, (prev, curr) => [curr > prev, Math.max(prev, curr)]))
|
|
176
|
-
.pipe(
|
|
177
|
-
map([], (acc, value) => {
|
|
178
|
-
const newAcc = [...acc, value].slice(-10);
|
|
179
|
-
const sum = newAcc.reduce((a, b) => a + b, 0);
|
|
180
|
-
return [{ value, sum, count: newAcc.length }, newAcc];
|
|
181
|
-
})
|
|
182
|
-
)
|
|
183
|
-
.pipe(group(0, (count, item) => [count >= 3, count >= 3 ? 0 : count + 1]));
|
|
184
|
-
|
|
185
|
-
// Combining multiple streams
|
|
186
|
-
const stream2 = new Stream<number>();
|
|
187
|
-
const merged = stream
|
|
188
|
-
.pipe(filter((x) => x % 2 === 0))
|
|
189
|
-
.pipe(merge(stream2))
|
|
190
|
-
.pipe(map((x) => x.toString()));
|
|
248
|
+
const stream1 = new Stream<number>();
|
|
249
|
+
const stream2 = new Stream<string>();
|
|
250
|
+
|
|
251
|
+
// Combine multiple streams with type safety
|
|
252
|
+
const combined = stream1.pipe(merge(stream2));
|
|
253
|
+
// Type: Stream<number | string>
|
|
254
|
+
|
|
255
|
+
combined.listen((value) => {
|
|
256
|
+
if (typeof value === "number") {
|
|
257
|
+
console.log("Number:", value);
|
|
258
|
+
} else {
|
|
259
|
+
console.log("String:", value);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
191
262
|
```
|
|
192
263
|
|
|
193
|
-
|
|
264
|
+
**[📖 Complete Merge Documentation →](src/transformers/merge.md)**
|
|
194
265
|
|
|
195
|
-
|
|
266
|
+
#### 4. Flat - Event Multiplication
|
|
196
267
|
|
|
197
268
|
```typescript
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
// Usage with perfect type inference
|
|
232
|
-
const searchInput = new Stream<string>();
|
|
269
|
+
import { flat } from "@soffinal/stream";
|
|
270
|
+
|
|
271
|
+
// Transform 1 array event → N individual events
|
|
272
|
+
const arrayStream = new Stream<number[]>();
|
|
273
|
+
const individualNumbers = arrayStream.pipe(flat());
|
|
274
|
+
|
|
275
|
+
arrayStream.push([1, 2, 3]); // Emits: 1, 2, 3 as separate events
|
|
276
|
+
|
|
277
|
+
// Configurable depth flattening
|
|
278
|
+
const deepArrays = new Stream<number[][][]>();
|
|
279
|
+
const flattened = deepArrays.pipe(flat(2)); // Flatten 2 levels deep
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**[📖 Complete Flat Documentation →](src/transformers/flat.md)**
|
|
283
|
+
|
|
284
|
+
### Documentation-as-Distribution: Copy-Paste Transformers
|
|
285
|
+
|
|
286
|
+
No separate repos, no CLI tools, no package management - just copy-paste ready transformers embedded in JSDoc!
|
|
287
|
+
|
|
288
|
+
But more importantly: **Documentation-as-Distribution is actually Education-as-Distribution.**
|
|
289
|
+
|
|
290
|
+
#### The Educational Transparency Revolution
|
|
291
|
+
|
|
292
|
+
Unlike traditional libraries where transformers are minified black boxes, our approach makes **every implementation pattern visible and learnable**:
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
// 📦 All transformers are copy-pastable from IntelliSense!
|
|
296
|
+
// Hover over 'Stream' to see the complete transformers library
|
|
297
|
+
|
|
298
|
+
// Example: Users don't just get functions - they get implementation education
|
|
299
|
+
const searchInput = new Stream<string>(); // ← Hover here for full library
|
|
233
300
|
const searchResults = searchInput
|
|
234
|
-
.pipe(
|
|
235
|
-
.pipe(
|
|
236
|
-
.pipe(
|
|
301
|
+
.pipe(distinct()) // Copy from Stream JSDoc - learn deduplication patterns
|
|
302
|
+
.pipe(simpleFilter((q) => q.length > 2)) // Copy from Stream JSDoc - learn filtering logic
|
|
303
|
+
.pipe(take(10)) // Copy from Stream JSDoc - learn termination patterns
|
|
304
|
+
.pipe(delay(300)) // Copy from Stream JSDoc - learn async transformation
|
|
305
|
+
.pipe(simpleMap((query) => searchAPI(query))); // Copy from Stream JSDoc - learn mapping patterns
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
#### What Users Actually Learn
|
|
309
|
+
|
|
310
|
+
When users hover over any function in JSDoc, they see **complete implementation patterns**:
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
// Users see EXACTLY how to build transformers
|
|
314
|
+
const take = <T>(n: number) =>
|
|
315
|
+
filter<T, { count: number }>({ count: 0 }, (state, value) => {
|
|
316
|
+
if (state.count >= n) return; // ← Learn termination patterns
|
|
317
|
+
return [true, { count: state.count + 1 }]; // ← Learn state evolution
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const distinct = <T>() =>
|
|
321
|
+
filter<T, { seen: Set<T> }>({ seen: new Set() }, (state, value) => {
|
|
322
|
+
if (state.seen.has(value)) return [false, state]; // ← Learn deduplication logic
|
|
323
|
+
state.seen.add(value); // ← Learn state mutation patterns
|
|
324
|
+
return [true, state];
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
#### From Consumers to Creators
|
|
329
|
+
|
|
330
|
+
This transparency empowers users to become **transformer architects**:
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
// After learning from JSDoc examples, users create their own:
|
|
334
|
+
const withTimestamp = <T>() =>
|
|
335
|
+
map<T, {}, { value: T; timestamp: number }>(
|
|
336
|
+
{}, // ← Learned: empty state when no memory needed
|
|
337
|
+
(_, value) => [
|
|
338
|
+
{ value, timestamp: Date.now() }, // ← Learned: transformation pattern
|
|
339
|
+
{}, // ← Learned: state management
|
|
340
|
+
]
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
const rateLimited = <T>(maxPerSecond: number) =>
|
|
344
|
+
filter<T, { timestamps: number[] }>({ timestamps: [] }, (state, value) => {
|
|
345
|
+
const now = Date.now();
|
|
346
|
+
const recent = state.timestamps.filter((t) => now - t < 1000);
|
|
347
|
+
if (recent.length >= maxPerSecond) return [false, { timestamps: recent }];
|
|
348
|
+
return [true, { timestamps: [...recent, now] }];
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
#### Benefits Beyond Bundle Size
|
|
353
|
+
|
|
354
|
+
**Traditional Libraries (Code-as-Distribution):**
|
|
355
|
+
|
|
356
|
+
- ❌ **Black box implementations** - Minified, unreadable code
|
|
357
|
+
- ❌ **Separate documentation** - Often outdated, disconnected from code
|
|
358
|
+
- ❌ **Limited extensibility** - Users can only use what's provided
|
|
359
|
+
- ❌ **Learning barrier** - No insight into implementation patterns
|
|
360
|
+
- ❌ **Bundle bloat** - Every transformer adds runtime cost
|
|
361
|
+
|
|
362
|
+
**Documentation-as-Distribution:**
|
|
363
|
+
|
|
364
|
+
- ✅ **Zero friction** - Copy-paste ready transformers
|
|
365
|
+
- ✅ **Perfect discoverability** - IntelliSense shows all available transformers
|
|
366
|
+
- ✅ **Always up-to-date** - Examples match current API version
|
|
367
|
+
- ✅ **No ecosystem fragmentation** - Everything in one place
|
|
368
|
+
- ✅ **Educational transparency** - Users learn implementation patterns
|
|
369
|
+
- ✅ **Infinite extensibility** - Users become transformer creators
|
|
370
|
+
- ✅ **Self-documenting** - Usage examples included with working code
|
|
371
|
+
- ✅ **Zero bundle cost** - JSDoc stripped at compile time
|
|
372
|
+
|
|
373
|
+
#### The Network Effect
|
|
374
|
+
|
|
375
|
+
Documentation-as-Distribution creates **multiplicative value**:
|
|
376
|
+
|
|
377
|
+
1. **User discovers** transformer in JSDoc
|
|
378
|
+
2. **User learns** implementation pattern
|
|
379
|
+
3. **User creates** custom transformers for their domain
|
|
380
|
+
4. **User shares** patterns with their team
|
|
381
|
+
5. **Team creates** hundreds of variations
|
|
382
|
+
6. **Knowledge multiplies** exponentially across the community
|
|
383
|
+
|
|
384
|
+
**How it works:**
|
|
385
|
+
|
|
386
|
+
1. Hover over `Stream` in your IDE to see the complete transformers library
|
|
387
|
+
2. Or hover over individual functions for quick references
|
|
388
|
+
3. Copy the transformer you need
|
|
389
|
+
4. Use immediately - perfect TypeScript inference included!
|
|
390
|
+
5. **Learn the patterns** and create your own infinite variations
|
|
391
|
+
|
|
392
|
+
**Available Transformers (via JSDoc):**
|
|
393
|
+
|
|
394
|
+
- `take(n)`, `skip(n)`, `distinct()`, `tap(fn)` - Essential filtering patterns
|
|
395
|
+
- `withIndex()`, `delay(ms)`, `pluck(key)`, `scan(fn, initial)` - Common transformation patterns
|
|
396
|
+
- `simpleFilter(predicate)` - Convenient filtering without state
|
|
397
|
+
- `simpleMap(fn)` - Convenient mapping without state
|
|
398
|
+
- `toState(initialValue)` - Convert streams to reactive state
|
|
399
|
+
- More transformers added with each release!
|
|
400
|
+
|
|
401
|
+
**📊 Bundle Size Impact:**
|
|
402
|
+
|
|
403
|
+
- **Package size**: Currently ~15KB, grows with JSDoc transformer examples over time
|
|
404
|
+
- **Your app bundle**: Always only 5.5KB (runtime code only, zero JSDoc overhead)
|
|
405
|
+
- **Tree-shaking**: Only imported functions included in final bundle
|
|
406
|
+
- **JSDoc transformers**: "Free" - rich transformer library without production cost
|
|
407
|
+
|
|
408
|
+
**You're not just building applications - you're learning a paradigm that scales infinitely.**
|
|
409
|
+
|
|
410
|
+
### Manual Composition
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
// You can still build transformers manually
|
|
414
|
+
const customTransformer = <T>(count: number) =>
|
|
415
|
+
filter<T, { taken: number }>({ taken: 0 }, (state, value) => {
|
|
416
|
+
if (state.taken >= count) return; // Terminate after N items
|
|
417
|
+
return [true, { taken: state.taken + 1 }];
|
|
418
|
+
});
|
|
237
419
|
```
|
|
238
420
|
|
|
239
|
-
### Reactive State
|
|
421
|
+
### Reactive State: Stateful Values
|
|
240
422
|
|
|
241
|
-
State
|
|
423
|
+
`State` extends `Stream` with a current value that can be read and written:
|
|
242
424
|
|
|
243
425
|
```typescript
|
|
244
426
|
const user = new State<User | null>(null);
|
|
245
427
|
const theme = new State<"light" | "dark">("light");
|
|
428
|
+
const counter = new State(0);
|
|
429
|
+
|
|
430
|
+
// Read current value
|
|
431
|
+
console.log(counter.value); // 0
|
|
432
|
+
|
|
433
|
+
// Write triggers all listeners
|
|
434
|
+
counter.value = 5;
|
|
246
435
|
|
|
247
|
-
//
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
436
|
+
// State from transformed streams
|
|
437
|
+
const source = new Stream<number>();
|
|
438
|
+
const derivedState = new State(0, source.pipe(map({}, (_, v) => [v * 2, {}])));
|
|
439
|
+
|
|
440
|
+
// Derived state using transformers
|
|
441
|
+
const isLoggedIn = user.pipe(map({}, (_, u) => [u !== null, {}]));
|
|
442
|
+
|
|
443
|
+
const userDisplayName = user.pipe(
|
|
444
|
+
filter({}, (_, u) => [u !== null, {}]),
|
|
445
|
+
map({}, (_, u) => [`${u.firstName} ${u.lastName}`, {}])
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
// Convert streams to state with toState transformer
|
|
449
|
+
const processedState = source
|
|
450
|
+
.pipe(filter({}, (_, v) => [v > 0, {}]))
|
|
451
|
+
.pipe(map({}, (_, v) => [v.toString(), {}]))
|
|
452
|
+
.pipe(toState("0")); // Explicit initial value
|
|
252
453
|
|
|
253
454
|
// Automatic UI updates
|
|
254
455
|
isLoggedIn.listen((loggedIn) => {
|
|
255
456
|
document.body.classList.toggle("authenticated", loggedIn);
|
|
256
457
|
});
|
|
458
|
+
|
|
459
|
+
// State changes propagate through the pipeline
|
|
460
|
+
user.value = { firstName: "John", lastName: "Doe" };
|
|
461
|
+
// Triggers: isLoggedIn → true, userDisplayName → 'John Doe'
|
|
257
462
|
```
|
|
258
463
|
|
|
259
|
-
### Reactive Collections
|
|
464
|
+
### Reactive Collections: Fine-Grained Change Events
|
|
260
465
|
|
|
261
|
-
Collections that emit
|
|
466
|
+
Collections that emit specific change events for efficient UI updates:
|
|
262
467
|
|
|
263
468
|
```typescript
|
|
469
|
+
import { List, Map, Set } from "@soffinal/stream";
|
|
470
|
+
|
|
264
471
|
const todos = new List<Todo>();
|
|
265
|
-
const
|
|
472
|
+
const userCache = new Map<string, User>();
|
|
266
473
|
const activeUsers = new Set<string>();
|
|
267
474
|
|
|
268
|
-
// React to
|
|
269
|
-
todos.insert.listen(([index, todo]) =>
|
|
270
|
-
|
|
271
|
-
|
|
475
|
+
// React to specific operations
|
|
476
|
+
todos.insert.listen(([index, todo]) => {
|
|
477
|
+
console.log(`Todo inserted at ${index}:`, todo);
|
|
478
|
+
renderTodoAtIndex(index, todo);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
todos.delete.listen(([index, todo]) => {
|
|
482
|
+
console.log(`Todo removed from ${index}:`, todo);
|
|
483
|
+
removeTodoFromDOM(index);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// Map changes
|
|
487
|
+
userCache.set.listen(([key, user]) => {
|
|
488
|
+
console.log(`User cached: ${key}`, user);
|
|
489
|
+
updateUserInUI(key, user);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Set changes
|
|
493
|
+
activeUsers.add.listen((userId) => {
|
|
494
|
+
console.log(`User ${userId} came online`);
|
|
495
|
+
showOnlineIndicator(userId);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
activeUsers.delete.listen((userId) => {
|
|
499
|
+
console.log(`User ${userId} went offline`);
|
|
500
|
+
hideOnlineIndicator(userId);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Use like normal collections
|
|
504
|
+
todos.push({ id: 1, text: "Learn streams", done: false });
|
|
505
|
+
userCache.set("user1", { name: "Alice", email: "alice@example.com" });
|
|
506
|
+
activeUsers.add("user1");
|
|
272
507
|
```
|
|
273
508
|
|
|
274
509
|
## API Reference
|
|
275
510
|
|
|
276
511
|
### Stream\<T>
|
|
277
512
|
|
|
513
|
+
#### Core Methods
|
|
514
|
+
|
|
515
|
+
- `push(...values: T[]): void` - Emit values to all listeners
|
|
516
|
+
- `listen(callback: (value: T) => void, signal?: AbortSignal | Stream<any>): () => void` - Add listener, returns cleanup
|
|
517
|
+
- `pipe<OUTPUT>(transformer: (stream: this) => OUTPUT): OUTPUT` - Apply any transformer (flexible return type)
|
|
518
|
+
|
|
519
|
+
#### Async Interface
|
|
520
|
+
|
|
521
|
+
- `then<U>(callback?: (value: T) => U): Promise<U>` - Promise interface for next value
|
|
522
|
+
- `[Symbol.asyncIterator](): AsyncIterator<T>` - Async iteration support
|
|
523
|
+
|
|
278
524
|
#### Properties
|
|
279
525
|
|
|
280
526
|
- `hasListeners: boolean` - Whether stream has active listeners
|
|
281
527
|
- `listenerAdded: Stream<void>` - Emits when listener is added
|
|
282
528
|
- `listenerRemoved: Stream<void>` - Emits when listener is removed
|
|
283
529
|
|
|
284
|
-
|
|
530
|
+
### State\<T> extends Stream\<T>
|
|
285
531
|
|
|
286
|
-
|
|
287
|
-
- `listen(callback: (value: T) => void, signal?: AbortSignal): () => void` - Add listener, returns cleanup function
|
|
288
|
-
- `filter<U extends T>(predicate: (value: T) => value is U): Stream<U>` - Filter with type guard
|
|
289
|
-
- `filter(predicate: (value: T) => boolean | Promise<boolean>): Stream<T>` - Filter with async predicate
|
|
290
|
-
- `filter<S>(initialState: S, accumulator: (state: S, value: T) => [boolean, S] | Promise<[boolean, S]>): Stream<T>` - Stateful filtering
|
|
291
|
-
- `map<U>(mapper: (value: T) => U | Promise<U>): Stream<U>` - Transform with async mapper
|
|
292
|
-
- `map<S, U>(initialState: S, accumulator: (state: S, value: T) => [U, S] | Promise<[U, S]>): Stream<U>` - Stateful mapping
|
|
293
|
-
- `group(predicate: (batch: T[]) => boolean | Promise<boolean>): Stream<T[]>` - Group into batches
|
|
294
|
-
- `group<S>(initialState: S, accumulator: (state: S, value: T) => [boolean, S] | Promise<[boolean, S]>): Stream<S>` - Stateful grouping
|
|
295
|
-
- `merge<STREAMS extends [Stream<any>, ...Stream<any>[]]>(...streams: STREAMS): Stream<T | ValueOf<STREAMS[number]>>` - Merge multiple streams
|
|
296
|
-
- `flat<DEPTH extends number = 0>(depth?: DEPTH): Stream<FlatArray<T, DEPTH>>` - Flatten arrays with configurable depth
|
|
297
|
-
- `pipe<U>(transformer: (stream: Stream<T>) => Stream<U>): Stream<U>` - Apply functional transformer
|
|
532
|
+
#### Constructor
|
|
298
533
|
|
|
299
|
-
|
|
534
|
+
- `new State(initialValue: T)` - Create state with initial value
|
|
535
|
+
- `new State(initialValue: T, stream: Stream<T>)` - Create state from stream
|
|
300
536
|
|
|
301
|
-
|
|
302
|
-
- `[Symbol.asyncIterator](): AsyncIterator<T>` - Async iteration
|
|
537
|
+
#### Additional Properties
|
|
303
538
|
|
|
304
|
-
|
|
539
|
+
- `value: T` - Current state value (get/set)
|
|
305
540
|
|
|
306
|
-
|
|
541
|
+
### Universal Transformers
|
|
307
542
|
|
|
308
|
-
|
|
543
|
+
#### filter(initialState, accumulator)
|
|
309
544
|
|
|
310
|
-
|
|
545
|
+
- **Simple**: `filter({}, (_, value) => [boolean, {}])`
|
|
546
|
+
- **Stateful**: `filter(state, (state, value) => [boolean, newState])`
|
|
547
|
+
- **Async**: `filter({}, async (_, value) => [boolean, {}])`
|
|
548
|
+
- **Termination**: Return `undefined` to terminate stream
|
|
311
549
|
|
|
312
|
-
|
|
550
|
+
#### map(initialState, accumulator)
|
|
313
551
|
|
|
314
|
-
|
|
315
|
-
- `
|
|
316
|
-
- `
|
|
317
|
-
- `filter<T, S>(initialState: S, accumulator: (state: S, value: T) => [boolean, S] | Promise<[boolean, S]>): (stream: Stream<T>) => Stream<T>`
|
|
552
|
+
- **Simple**: `map({}, (_, value) => [newValue, {}])`
|
|
553
|
+
- **Stateful**: `map(state, (state, value) => [newValue, newState])`
|
|
554
|
+
- **Async**: `map({}, async (_, value) => [newValue, {}])`
|
|
318
555
|
|
|
319
|
-
####
|
|
320
|
-
- `map<T, U>(mapper: (value: T) => U | Promise<U>): (stream: Stream<T>) => Stream<U>`
|
|
321
|
-
- `map<T, S, U>(initialState: S, accumulator: (state: S, value: T) => [U, S] | Promise<[U, S]>): (stream: Stream<T>) => Stream<U>`
|
|
556
|
+
#### merge(...streams)
|
|
322
557
|
|
|
323
|
-
|
|
324
|
-
-
|
|
325
|
-
-
|
|
558
|
+
- **Basic**: `stream.pipe(merge(stream2, stream3))`
|
|
559
|
+
- **Type-Safe**: Automatically creates union types
|
|
560
|
+
- **Temporal Order**: Maintains chronological sequence
|
|
326
561
|
|
|
327
|
-
####
|
|
328
|
-
- `merge<STREAMS extends [Stream<any>, ...Stream<any>[]]>(...streams: STREAMS): <T>(stream: Stream<T>) => Stream<T | ValueOf<STREAMS[number]>>`
|
|
562
|
+
#### flat(depth?)
|
|
329
563
|
|
|
330
|
-
|
|
331
|
-
- `flat()
|
|
332
|
-
-
|
|
564
|
+
- **Basic**: `stream.pipe(flat())` - Flatten one level
|
|
565
|
+
- **Deep**: `stream.pipe(flat(2))` - Flatten N levels
|
|
566
|
+
- **Event Multiplication**: 1 array event → N individual events
|
|
333
567
|
|
|
334
568
|
### Reactive Collections
|
|
335
569
|
|
|
@@ -351,111 +585,22 @@ Functional transformers for use with `pipe()` - all support async operations:
|
|
|
351
585
|
- `delete: Stream<T>` - Delete events
|
|
352
586
|
- `clear: Stream<void>` - Clear events
|
|
353
587
|
|
|
354
|
-
## Examples
|
|
355
|
-
|
|
356
|
-
### Real-time Data Processing
|
|
357
|
-
|
|
358
|
-
```typescript
|
|
359
|
-
const sensorData = new Stream<SensorReading>();
|
|
360
|
-
|
|
361
|
-
// Multi-stage processing pipeline
|
|
362
|
-
const alerts = sensorData
|
|
363
|
-
.filter((reading) => reading.temperature > 30)
|
|
364
|
-
.map((reading) => ({ ...reading, timestamp: Date.now() }))
|
|
365
|
-
.group((batch) => batch.length >= 5);
|
|
366
|
-
|
|
367
|
-
alerts.listen((batch) => sendAlertNotification(batch));
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
### WebSocket Integration
|
|
371
|
-
|
|
372
|
-
```typescript
|
|
373
|
-
const wsStream = new Stream<MessageEvent>(async function* () {
|
|
374
|
-
const ws = new WebSocket("wss://api.example.com");
|
|
375
|
-
|
|
376
|
-
while (ws.readyState !== WebSocket.CLOSED) {
|
|
377
|
-
yield await new Promise((resolve) => {
|
|
378
|
-
ws.onmessage = resolve;
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
const messages = wsStream.map((event) => JSON.parse(event.data)).filter((data) => data.type === "update");
|
|
384
|
-
|
|
385
|
-
messages.listen((update) => handleUpdate(update));
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
### State Management
|
|
389
|
-
|
|
390
|
-
```typescript
|
|
391
|
-
interface AppState {
|
|
392
|
-
user: User | null;
|
|
393
|
-
theme: "light" | "dark";
|
|
394
|
-
notifications: Notification[];
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const appState = new State<AppState>({
|
|
398
|
-
user: null,
|
|
399
|
-
theme: "light",
|
|
400
|
-
notifications: [],
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
// Derived state
|
|
404
|
-
const unreadCount = appState.map((state) => state.notifications.filter((n) => !n.read).length);
|
|
405
|
-
|
|
406
|
-
// Reactive UI
|
|
407
|
-
unreadCount.listen((count) => {
|
|
408
|
-
document.title = count > 0 ? `(${count}) App` : "App";
|
|
409
|
-
});
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
### Browser Counter Example
|
|
413
|
-
|
|
414
|
-
```html
|
|
415
|
-
<!DOCTYPE html>
|
|
416
|
-
<html>
|
|
417
|
-
<head>
|
|
418
|
-
<title>Reactive Counter</title>
|
|
419
|
-
</head>
|
|
420
|
-
<body>
|
|
421
|
-
<div id="counter">0</div>
|
|
422
|
-
<button id="increment">+</button>
|
|
423
|
-
<button id="decrement">-</button>
|
|
424
|
-
|
|
425
|
-
<script type="module">
|
|
426
|
-
import { State } from "https://esm.sh/@soffinal/stream";
|
|
427
|
-
|
|
428
|
-
const count = new State(0);
|
|
429
|
-
const display = document.getElementById("counter");
|
|
430
|
-
|
|
431
|
-
// Reactive UI updates
|
|
432
|
-
count.listen((value) => (display.textContent = value));
|
|
433
|
-
|
|
434
|
-
// User interactions
|
|
435
|
-
document.getElementById("increment").onclick = () => count.value++;
|
|
436
|
-
document.getElementById("decrement").onclick = () => count.value--;
|
|
437
|
-
</script>
|
|
438
|
-
</body>
|
|
439
|
-
</html>
|
|
440
|
-
```
|
|
441
|
-
|
|
442
588
|
## Performance
|
|
443
589
|
|
|
444
|
-
|
|
590
|
+
### Bundle Size
|
|
445
591
|
|
|
446
|
-
- **
|
|
447
|
-
- **
|
|
448
|
-
- **
|
|
449
|
-
- **
|
|
592
|
+
- **Runtime bundle** - 5.5KB minified, 1.6KB gzipped
|
|
593
|
+
- **Package size** - Starts small, grows with JSDoc transformer library
|
|
594
|
+
- **Your production app** - Always gets only the 5.5KB runtime code
|
|
595
|
+
- **Tree-shakeable** - Import only what you use
|
|
450
596
|
|
|
451
|
-
###
|
|
597
|
+
### Benchmarks
|
|
452
598
|
|
|
453
|
-
- **
|
|
454
|
-
- **Efficient
|
|
455
|
-
- **
|
|
456
|
-
- **Automatic cleanup** prevents memory leaks
|
|
599
|
+
- **Fast startup** - Zero dependencies, instant initialization
|
|
600
|
+
- **Efficient pipelines** - Optimized transformer composition
|
|
601
|
+
- **Memory bounded** - Built-in backpressure handling
|
|
457
602
|
|
|
458
|
-
##
|
|
603
|
+
## Runtime Support
|
|
459
604
|
|
|
460
605
|
- **Modern browsers** supporting ES2020+
|
|
461
606
|
- **Node.js** 16+
|
|
@@ -465,28 +610,6 @@ unreadCount.listen((count) => {
|
|
|
465
610
|
|
|
466
611
|
## Migration Guide
|
|
467
612
|
|
|
468
|
-
### From RxJS
|
|
469
|
-
|
|
470
|
-
```typescript
|
|
471
|
-
// RxJS
|
|
472
|
-
import { Subject, map, filter } from "rxjs";
|
|
473
|
-
const subject = new Subject();
|
|
474
|
-
subject
|
|
475
|
-
.pipe(
|
|
476
|
-
filter((x) => x > 0),
|
|
477
|
-
map((x) => x * 2)
|
|
478
|
-
)
|
|
479
|
-
.subscribe(console.log);
|
|
480
|
-
|
|
481
|
-
// @soffinal/stream
|
|
482
|
-
import { Stream } from "@soffinal/stream";
|
|
483
|
-
const stream = new Stream();
|
|
484
|
-
stream
|
|
485
|
-
.filter((x) => x > 0)
|
|
486
|
-
.map((x) => x * 2)
|
|
487
|
-
.listen(console.log);
|
|
488
|
-
```
|
|
489
|
-
|
|
490
613
|
### From EventEmitter
|
|
491
614
|
|
|
492
615
|
```typescript
|
|
@@ -503,6 +626,24 @@ stream.listen(console.log);
|
|
|
503
626
|
stream.push("hello");
|
|
504
627
|
```
|
|
505
628
|
|
|
629
|
+
## Documentation
|
|
630
|
+
|
|
631
|
+
### Transformer Guides
|
|
632
|
+
|
|
633
|
+
- **[Filter Transformer →](src/transformers/filter.md)** - Adaptive constraints and stream termination
|
|
634
|
+
- **[Map Transformer →](src/transformers/map.md)** - Stateful transformations and async processing
|
|
635
|
+
- **[Merge Transformer →](src/transformers/merge.md)** - Stream orchestration and type-safe combination
|
|
636
|
+
- **[Flat Transformer →](src/transformers/flat.md)** - Event multiplication and array flattening
|
|
637
|
+
|
|
638
|
+
### Philosophy
|
|
639
|
+
|
|
640
|
+
**Adaptive Reactive Programming** - A new paradigm where transformers maintain state and evolve their behavior based on stream history. This enables:
|
|
641
|
+
|
|
642
|
+
- **Learning transformers** that adapt to data patterns
|
|
643
|
+
- **Stateful operations** with memory between events
|
|
644
|
+
- **Stream termination** for lifecycle control
|
|
645
|
+
- **Zero-overhead types** with perfect inference
|
|
646
|
+
|
|
506
647
|
## Contributing
|
|
507
648
|
|
|
508
649
|
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
@@ -525,5 +666,5 @@ Contact: <smari.sofiane@gmail.com>
|
|
|
525
666
|
---
|
|
526
667
|
|
|
527
668
|
<div align="center">
|
|
528
|
-
<strong>
|
|
669
|
+
<strong>Pioneering Adaptive Reactive Programming</strong>
|
|
529
670
|
</div>
|