@soffinal/stream 0.1.4 → 0.2.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/CHANGELOG.md +22 -0
- package/README.md +318 -299
- 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} +1 -1
- package/dist/reactive/list.d.ts.map +1 -0
- package/dist/{map.d.ts → reactive/map.d.ts} +1 -1
- package/dist/reactive/map.d.ts.map +1 -0
- package/dist/{set.d.ts → reactive/set.d.ts} +1 -1
- package/dist/reactive/set.d.ts.map +1 -0
- package/dist/{state.d.ts → reactive/state.d.ts} +1 -1
- package/dist/reactive/state.d.ts.map +1 -0
- package/dist/stream.d.ts +1 -279
- package/dist/stream.d.ts.map +1 -1
- package/dist/transformers/filter.d.ts +64 -0
- package/dist/transformers/filter.d.ts.map +1 -0
- package/dist/transformers/flat.d.ts +29 -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 +64 -0
- package/dist/transformers/map.d.ts.map +1 -0
- package/dist/transformers/merge.d.ts +33 -0
- package/dist/transformers/merge.d.ts.map +1 -0
- package/package.json +5 -8
- package/src/transformers/filter.md +161 -0
- package/src/transformers/flat.md +56 -0
- package/src/transformers/map.md +184 -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
|
|
|
@@ -20,46 +20,73 @@ A modern, async-first streaming library that treats asynchronous data flow as a
|
|
|
20
20
|
- [Performance](#performance)
|
|
21
21
|
- [Browser Support](#browser-support)
|
|
22
22
|
- [Migration Guide](#migration-guide)
|
|
23
|
+
- [Documentation](#documentation)
|
|
23
24
|
- [Contributing](#contributing)
|
|
24
25
|
- [License](#license)
|
|
25
26
|
|
|
26
27
|
## Features
|
|
27
28
|
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
- 📦 **Zero Dependencies** - Lightweight
|
|
29
|
+
- 🧠 **Adaptive Constraints** - Transformers that learn and evolve based on stream history
|
|
30
|
+
- 🔧 **Universal Primitives** - Four algebraic primitives: `filter`, `map`, `merge`, `flat`
|
|
31
|
+
- 📚 **Documentation-as-Distribution** - Copy-paste transformers embedded in JSDoc, no separate packages needed
|
|
32
|
+
- ⚡ **Async-First** - All operations support async with order preservation
|
|
33
|
+
- 🔄 **Multicast Streams** - One stream, unlimited consumers
|
|
34
|
+
- ⏳ **Awaitable** - `await stream` for next value
|
|
35
|
+
- 🔁 **Async Iterable** - Native `for await` loop support
|
|
36
|
+
- 🛠️ **Pipe Composition** - Functional transformer composition
|
|
37
|
+
- 📊 **Reactive State** - Stateful values with automatic change propagation
|
|
38
|
+
- 📋 **Reactive Collections** - Lists, Maps, Sets with fine-grained events
|
|
39
|
+
- 🗑️ **Stream Termination** - Declarative stream lifecycle control
|
|
40
|
+
- 📦 **Zero Dependencies** - Lightweight and tree-shakeable
|
|
40
41
|
- 🌐 **Universal** - Node.js, browsers, Deno, Bun, Cloudflare Workers
|
|
41
|
-
- 📘 **Full TypeScript** - Complete type safety
|
|
42
|
+
- 📘 **Full TypeScript** - Complete type safety without the burden
|
|
42
43
|
|
|
43
44
|
## Quick Start
|
|
44
45
|
|
|
45
46
|
```typescript
|
|
46
|
-
import { Stream, State } from "@soffinal/stream";
|
|
47
|
+
import { Stream, State, filter, map, merge } from "@soffinal/stream";
|
|
47
48
|
|
|
48
|
-
// Create reactive
|
|
49
|
+
// Create reactive streams
|
|
49
50
|
const events = new Stream<string>();
|
|
50
|
-
const
|
|
51
|
+
const numbers = new Stream<number>();
|
|
51
52
|
|
|
52
|
-
//
|
|
53
|
-
const processed = events
|
|
53
|
+
// Pipe-based transformation with Adaptive Constraints
|
|
54
|
+
const processed = events
|
|
55
|
+
.pipe(simpleFilter((msg) => msg.length > 3)) // Simple filtering
|
|
56
|
+
.pipe(simpleMap((msg) => msg.toUpperCase())); // Transform to uppercase
|
|
54
57
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
// Stateful transformers that learn and adapt
|
|
59
|
+
const runningAverage = numbers
|
|
60
|
+
.pipe(
|
|
61
|
+
filter({ count: 0 }, (state, value) => {
|
|
62
|
+
// Only pass every 3rd number, terminate after 10
|
|
63
|
+
if (state.count >= 10) return; // Stream termination
|
|
64
|
+
return [(state.count + 1) % 3 === 0, { count: state.count + 1 }];
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
.pipe(
|
|
68
|
+
map({ sum: 0, count: 0 }, (state, value) => {
|
|
69
|
+
const newSum = state.sum + value;
|
|
70
|
+
const newCount = state.count + 1;
|
|
71
|
+
const average = newSum / newCount;
|
|
72
|
+
return [
|
|
73
|
+
{ value, average },
|
|
74
|
+
{ sum: newSum, count: newCount },
|
|
75
|
+
];
|
|
76
|
+
})
|
|
77
|
+
);
|
|
58
78
|
|
|
59
|
-
//
|
|
60
|
-
|
|
79
|
+
// Copy-paste transformers from JSDoc
|
|
80
|
+
const limited = numbers.pipe(take(5)); // Limit to 5 items
|
|
81
|
+
const indexed = events.pipe(withIndex()); // Add indices
|
|
82
|
+
const delayed = processed.pipe(delay(100)); // Delay each value
|
|
83
|
+
|
|
84
|
+
// Multiple consumers
|
|
85
|
+
processed.listen((msg) => console.log("Processed:", msg));
|
|
86
|
+
runningAverage.listen(({ value, average }) => console.log(`Value: ${value}, Running Average: ${average}`));
|
|
61
87
|
|
|
62
|
-
// Reactive state
|
|
88
|
+
// Reactive state
|
|
89
|
+
const counter = new State(0);
|
|
63
90
|
counter.listen((count) => (document.title = `Count: ${count}`));
|
|
64
91
|
counter.value++; // UI updates automatically
|
|
65
92
|
```
|
|
@@ -102,234 +129,319 @@ deno add jsr:@soffinal/stream
|
|
|
102
129
|
|
|
103
130
|
## Core Concepts
|
|
104
131
|
|
|
105
|
-
### Streams:
|
|
132
|
+
### Streams: Multicast Event Pipelines
|
|
106
133
|
|
|
107
|
-
A `Stream` is
|
|
134
|
+
A `Stream` is a multicast, async iterable that pushes values to multiple listeners while being awaitable for the next value.
|
|
108
135
|
|
|
109
136
|
```typescript
|
|
110
137
|
const userEvents = new Stream<UserEvent>();
|
|
111
138
|
|
|
112
|
-
// Multiple consumers
|
|
139
|
+
// Multiple consumers automatically share the same data
|
|
113
140
|
userEvents.listen((event) => analytics.track(event));
|
|
114
141
|
userEvents.listen((event) => notifications.send(event));
|
|
115
142
|
userEvents.listen((event) => database.save(event));
|
|
116
143
|
|
|
117
|
-
//
|
|
144
|
+
// Await the next event
|
|
118
145
|
const nextEvent = await userEvents;
|
|
146
|
+
|
|
147
|
+
// Async iteration
|
|
148
|
+
for await (const event of userEvents) {
|
|
149
|
+
if (event.type === "critical") break;
|
|
150
|
+
processEvent(event);
|
|
151
|
+
}
|
|
119
152
|
```
|
|
120
153
|
|
|
121
|
-
###
|
|
154
|
+
### Universal Primitives: The Four Algebraic Operations
|
|
155
|
+
|
|
156
|
+
All stream operations are built from four universal primitives with **Adaptive Constraints**:
|
|
122
157
|
|
|
123
|
-
####
|
|
158
|
+
#### 1. Filter - Adaptive Gatekeeper
|
|
124
159
|
|
|
125
160
|
```typescript
|
|
126
|
-
|
|
161
|
+
import { filter } from "@soffinal/stream";
|
|
127
162
|
|
|
128
|
-
// Simple
|
|
129
|
-
|
|
130
|
-
.filter((x) => x > 0)
|
|
131
|
-
.map((x) => x * 2)
|
|
132
|
-
.group((batch) => batch.length >= 5);
|
|
163
|
+
// Simple filtering
|
|
164
|
+
stream.pipe(filter({}, (_, value) => [value > 0, {}]));
|
|
133
165
|
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
166
|
+
// Stateful filtering with termination
|
|
167
|
+
stream.pipe(
|
|
168
|
+
filter({ count: 0 }, (state, value) => {
|
|
169
|
+
if (state.count >= 10) return; // Terminate after 10 items
|
|
170
|
+
return [value > 0, { count: state.count + 1 }];
|
|
171
|
+
})
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Async filtering
|
|
175
|
+
stream.pipe(
|
|
176
|
+
filter({}, async (_, value) => {
|
|
137
177
|
const isValid = await validateAsync(value);
|
|
138
|
-
return isValid;
|
|
178
|
+
return [isValid, {}];
|
|
139
179
|
})
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return { original: value, enriched };
|
|
143
|
-
});
|
|
180
|
+
);
|
|
181
|
+
```
|
|
144
182
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
183
|
+
**[📖 Complete Filter Documentation →](src/transformers/filter.md)**
|
|
184
|
+
|
|
185
|
+
#### 2. Map - Adaptive Transformer
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
import { map } from "@soffinal/stream";
|
|
189
|
+
|
|
190
|
+
// Simple transformation
|
|
191
|
+
stream.pipe(map({}, (_, value) => [value * 2, {}]));
|
|
192
|
+
|
|
193
|
+
// Stateful transformation with context
|
|
194
|
+
stream.pipe(
|
|
195
|
+
map({ sum: 0 }, (state, value) => {
|
|
196
|
+
const newSum = state.sum + value;
|
|
197
|
+
return [{ value, runningSum: newSum }, { sum: newSum }];
|
|
152
198
|
})
|
|
153
|
-
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Async transformation with order preservation
|
|
202
|
+
stream.pipe(
|
|
203
|
+
map({}, async (_, value) => {
|
|
204
|
+
const enriched = await enrichWithAPI(value);
|
|
205
|
+
return [{ original: value, enriched }, {}];
|
|
206
|
+
})
|
|
207
|
+
);
|
|
154
208
|
```
|
|
155
209
|
|
|
156
|
-
|
|
210
|
+
**[📖 Complete Map Documentation →](src/transformers/map.md)**
|
|
211
|
+
|
|
212
|
+
#### 3. Merge - Stream Orchestration
|
|
157
213
|
|
|
158
214
|
```typescript
|
|
159
|
-
import {
|
|
160
|
-
|
|
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()));
|
|
215
|
+
import { merge } from "@soffinal/stream";
|
|
216
|
+
|
|
217
|
+
const stream1 = new Stream<number>();
|
|
218
|
+
const stream2 = new Stream<string>();
|
|
219
|
+
|
|
220
|
+
// Combine multiple streams with type safety
|
|
221
|
+
const combined = stream1.pipe(merge(stream2));
|
|
222
|
+
// Type: Stream<number | string>
|
|
223
|
+
|
|
224
|
+
combined.listen((value) => {
|
|
225
|
+
if (typeof value === "number") {
|
|
226
|
+
console.log("Number:", value);
|
|
227
|
+
} else {
|
|
228
|
+
console.log("String:", value);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
191
231
|
```
|
|
192
232
|
|
|
193
|
-
|
|
233
|
+
**[📖 Complete Merge Documentation →](src/transformers/merge.md)**
|
|
194
234
|
|
|
195
|
-
|
|
235
|
+
#### 4. Flat - Event Multiplication
|
|
196
236
|
|
|
197
237
|
```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
|
-
yield value;
|
|
225
|
-
lastEmit = now;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
// Usage with perfect type inference
|
|
238
|
+
import { flat } from "@soffinal/stream";
|
|
239
|
+
|
|
240
|
+
// Transform 1 array event → N individual events
|
|
241
|
+
const arrayStream = new Stream<number[]>();
|
|
242
|
+
const individualNumbers = arrayStream.pipe(flat());
|
|
243
|
+
|
|
244
|
+
arrayStream.push([1, 2, 3]); // Emits: 1, 2, 3 as separate events
|
|
245
|
+
|
|
246
|
+
// Configurable depth flattening
|
|
247
|
+
const deepArrays = new Stream<number[][][]>();
|
|
248
|
+
const flattened = deepArrays.pipe(flat(2)); // Flatten 2 levels deep
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**[📖 Complete Flat Documentation →](src/transformers/flat.md)**
|
|
252
|
+
|
|
253
|
+
### Documentation-as-Distribution: Revolutionary Copy-Paste Transformers
|
|
254
|
+
|
|
255
|
+
**🚀 World's First Documentation-as-Distribution System**
|
|
256
|
+
|
|
257
|
+
No separate repos, no CLI tools, no package management - just copy-paste ready transformers embedded in JSDoc!
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// 📦 All transformers are copy-pastable from IntelliSense!
|
|
261
|
+
// Just hover over any primitive in your IDE to see the transformer library
|
|
262
|
+
|
|
263
|
+
// Example: Essential transformers available via autocomplete
|
|
232
264
|
const searchInput = new Stream<string>();
|
|
233
265
|
const searchResults = searchInput
|
|
234
|
-
.pipe(
|
|
235
|
-
.pipe(
|
|
236
|
-
.pipe(
|
|
266
|
+
.pipe(distinct()) // Copy from filter() JSDoc
|
|
267
|
+
.pipe(simpleFilter((q) => q.length > 2)) // Copy from filter() JSDoc
|
|
268
|
+
.pipe(take(10)) // Copy from filter() JSDoc
|
|
269
|
+
.pipe(delay(300)) // Copy from map() JSDoc
|
|
270
|
+
.pipe(simpleMap((query) => searchAPI(query))); // Copy from map() JSDoc
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Benefits:**
|
|
274
|
+
|
|
275
|
+
- ✅ **Zero friction** - Copy-paste ready transformers
|
|
276
|
+
- ✅ **Perfect discoverability** - IntelliSense shows all available transformers
|
|
277
|
+
- ✅ **Always up-to-date** - Examples match current API version
|
|
278
|
+
- ✅ **No ecosystem fragmentation** - Everything in one place
|
|
279
|
+
- ✅ **Self-documenting** - Usage examples included
|
|
280
|
+
|
|
281
|
+
**How it works:**
|
|
282
|
+
|
|
283
|
+
1. Hover over `filter()`, `map()`, `merge()`, or `flat()` in your IDE
|
|
284
|
+
2. Browse the 📦 COPY-PASTE TRANSFORMER examples
|
|
285
|
+
3. Copy the transformer you need
|
|
286
|
+
4. Use immediately - perfect TypeScript inference included!
|
|
287
|
+
|
|
288
|
+
**Available Transformers (via JSDoc):**
|
|
289
|
+
|
|
290
|
+
- `take(n)`, `skip(n)`, `distinct()` - Essential filtering
|
|
291
|
+
- `withIndex()`, `delay(ms)`, `pluck(key)` - Common transformations
|
|
292
|
+
- `simpleFilter(predicate)` - Convenient filtering without state
|
|
293
|
+
- `simpleMap(fn)` - Convenient mapping without state
|
|
294
|
+
- More transformers added with each release!
|
|
295
|
+
|
|
296
|
+
**📊 Bundle Size Impact:**
|
|
297
|
+
|
|
298
|
+
- **Package size**: Currently ~15KB, grows with JSDoc transformer examples over time
|
|
299
|
+
- **Your app bundle**: Always only 5.5KB (runtime code only, zero JSDoc overhead)
|
|
300
|
+
- **Tree-shaking**: Only imported functions included in final bundle
|
|
301
|
+
- **JSDoc transformers**: "Free" - rich transformer library without production cost
|
|
302
|
+
|
|
303
|
+
### Manual Composition
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
// You can still build transformers manually
|
|
307
|
+
const customTransformer = <T>(count: number) =>
|
|
308
|
+
filter<T, { taken: number }>({ taken: 0 }, (state, value) => {
|
|
309
|
+
if (state.taken >= count) return; // Terminate after N items
|
|
310
|
+
return [true, { taken: state.taken + 1 }];
|
|
311
|
+
});
|
|
237
312
|
```
|
|
238
313
|
|
|
239
|
-
### Reactive State
|
|
314
|
+
### Reactive State: Stateful Values
|
|
240
315
|
|
|
241
|
-
State
|
|
316
|
+
`State` extends `Stream` with a current value that can be read and written:
|
|
242
317
|
|
|
243
318
|
```typescript
|
|
244
319
|
const user = new State<User | null>(null);
|
|
245
320
|
const theme = new State<"light" | "dark">("light");
|
|
321
|
+
const counter = new State(0);
|
|
322
|
+
|
|
323
|
+
// Read current value
|
|
324
|
+
console.log(counter.value); // 0
|
|
246
325
|
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
326
|
+
// Write triggers all listeners
|
|
327
|
+
counter.value = 5;
|
|
328
|
+
|
|
329
|
+
// Derived state using transformers
|
|
330
|
+
const isLoggedIn = user.pipe(map({}, (_, u) => [u !== null, {}]));
|
|
331
|
+
|
|
332
|
+
const userDisplayName = user.pipe(
|
|
333
|
+
filter({}, (_, u) => [u !== null, {}]),
|
|
334
|
+
map({}, (_, u) => [`${u.firstName} ${u.lastName}`, {}])
|
|
335
|
+
);
|
|
252
336
|
|
|
253
337
|
// Automatic UI updates
|
|
254
338
|
isLoggedIn.listen((loggedIn) => {
|
|
255
339
|
document.body.classList.toggle("authenticated", loggedIn);
|
|
256
340
|
});
|
|
341
|
+
|
|
342
|
+
// State changes propagate through the pipeline
|
|
343
|
+
user.value = { firstName: "John", lastName: "Doe" };
|
|
344
|
+
// Triggers: isLoggedIn → true, userDisplayName → 'John Doe'
|
|
257
345
|
```
|
|
258
346
|
|
|
259
|
-
### Reactive Collections
|
|
347
|
+
### Reactive Collections: Fine-Grained Change Events
|
|
260
348
|
|
|
261
|
-
Collections that emit
|
|
349
|
+
Collections that emit specific change events for efficient UI updates:
|
|
262
350
|
|
|
263
351
|
```typescript
|
|
352
|
+
import { List, Map, Set } from "@soffinal/stream";
|
|
353
|
+
|
|
264
354
|
const todos = new List<Todo>();
|
|
265
|
-
const
|
|
355
|
+
const userCache = new Map<string, User>();
|
|
266
356
|
const activeUsers = new Set<string>();
|
|
267
357
|
|
|
268
|
-
// React to
|
|
269
|
-
todos.insert.listen(([index, todo]) =>
|
|
270
|
-
|
|
271
|
-
|
|
358
|
+
// React to specific operations
|
|
359
|
+
todos.insert.listen(([index, todo]) => {
|
|
360
|
+
console.log(`Todo inserted at ${index}:`, todo);
|
|
361
|
+
renderTodoAtIndex(index, todo);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
todos.delete.listen(([index, todo]) => {
|
|
365
|
+
console.log(`Todo removed from ${index}:`, todo);
|
|
366
|
+
removeTodoFromDOM(index);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Map changes
|
|
370
|
+
userCache.set.listen(([key, user]) => {
|
|
371
|
+
console.log(`User cached: ${key}`, user);
|
|
372
|
+
updateUserInUI(key, user);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Set changes
|
|
376
|
+
activeUsers.add.listen((userId) => {
|
|
377
|
+
console.log(`User ${userId} came online`);
|
|
378
|
+
showOnlineIndicator(userId);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
activeUsers.delete.listen((userId) => {
|
|
382
|
+
console.log(`User ${userId} went offline`);
|
|
383
|
+
hideOnlineIndicator(userId);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Use like normal collections
|
|
387
|
+
todos.push({ id: 1, text: "Learn streams", done: false });
|
|
388
|
+
userCache.set("user1", { name: "Alice", email: "alice@example.com" });
|
|
389
|
+
activeUsers.add("user1");
|
|
272
390
|
```
|
|
273
391
|
|
|
274
392
|
## API Reference
|
|
275
393
|
|
|
276
394
|
### Stream\<T>
|
|
277
395
|
|
|
278
|
-
####
|
|
279
|
-
|
|
280
|
-
- `hasListeners: boolean` - Whether stream has active listeners
|
|
281
|
-
- `listenerAdded: Stream<void>` - Emits when listener is added
|
|
282
|
-
- `listenerRemoved: Stream<void>` - Emits when listener is removed
|
|
283
|
-
|
|
284
|
-
#### Methods
|
|
396
|
+
#### Core Methods
|
|
285
397
|
|
|
286
398
|
- `push(...values: T[]): void` - Emit values to all listeners
|
|
287
|
-
- `listen(callback: (value: T) => void, signal?: AbortSignal): () => void` - Add listener, returns cleanup
|
|
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
|
|
399
|
+
- `listen(callback: (value: T) => void, signal?: AbortSignal | Stream<any>): () => void` - Add listener, returns cleanup
|
|
297
400
|
- `pipe<U>(transformer: (stream: Stream<T>) => Stream<U>): Stream<U>` - Apply functional transformer
|
|
298
401
|
|
|
299
402
|
#### Async Interface
|
|
300
403
|
|
|
301
|
-
- `then<U>(callback?: (value: T) => U): Promise<U>` - Promise interface
|
|
302
|
-
- `[Symbol.asyncIterator](): AsyncIterator<T>` - Async iteration
|
|
404
|
+
- `then<U>(callback?: (value: T) => U): Promise<U>` - Promise interface for next value
|
|
405
|
+
- `[Symbol.asyncIterator](): AsyncIterator<T>` - Async iteration support
|
|
406
|
+
|
|
407
|
+
#### Properties
|
|
408
|
+
|
|
409
|
+
- `hasListeners: boolean` - Whether stream has active listeners
|
|
410
|
+
- `listenerAdded: Stream<void>` - Emits when listener is added
|
|
411
|
+
- `listenerRemoved: Stream<void>` - Emits when listener is removed
|
|
303
412
|
|
|
304
413
|
### State\<T> extends Stream\<T>
|
|
305
414
|
|
|
306
|
-
#### Properties
|
|
415
|
+
#### Additional Properties
|
|
307
416
|
|
|
308
417
|
- `value: T` - Current state value (get/set)
|
|
309
418
|
|
|
310
|
-
###
|
|
419
|
+
### Universal Transformers
|
|
420
|
+
|
|
421
|
+
#### filter(initialState, accumulator)
|
|
422
|
+
|
|
423
|
+
- **Simple**: `filter({}, (_, value) => [boolean, {}])`
|
|
424
|
+
- **Stateful**: `filter(state, (state, value) => [boolean, newState])`
|
|
425
|
+
- **Async**: `filter({}, async (_, value) => [boolean, {}])`
|
|
426
|
+
- **Termination**: Return `undefined` to terminate stream
|
|
311
427
|
|
|
312
|
-
|
|
428
|
+
#### map(initialState, accumulator)
|
|
313
429
|
|
|
314
|
-
|
|
315
|
-
- `
|
|
316
|
-
- `
|
|
317
|
-
- `filter<T, S>(initialState: S, accumulator: (state: S, value: T) => [boolean, S] | Promise<[boolean, S]>): (stream: Stream<T>) => Stream<T>`
|
|
430
|
+
- **Simple**: `map({}, (_, value) => [newValue, {}])`
|
|
431
|
+
- **Stateful**: `map(state, (state, value) => [newValue, newState])`
|
|
432
|
+
- **Async**: `map({}, async (_, value) => [newValue, {}])`
|
|
318
433
|
|
|
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>`
|
|
434
|
+
#### merge(...streams)
|
|
322
435
|
|
|
323
|
-
|
|
324
|
-
-
|
|
325
|
-
-
|
|
436
|
+
- **Basic**: `stream.pipe(merge(stream2, stream3))`
|
|
437
|
+
- **Type-Safe**: Automatically creates union types
|
|
438
|
+
- **Temporal Order**: Maintains chronological sequence
|
|
326
439
|
|
|
327
|
-
####
|
|
328
|
-
- `merge<STREAMS extends [Stream<any>, ...Stream<any>[]]>(...streams: STREAMS): <T>(stream: Stream<T>) => Stream<T | ValueOf<STREAMS[number]>>`
|
|
440
|
+
#### flat(depth?)
|
|
329
441
|
|
|
330
|
-
|
|
331
|
-
- `flat()
|
|
332
|
-
-
|
|
442
|
+
- **Basic**: `stream.pipe(flat())` - Flatten one level
|
|
443
|
+
- **Deep**: `stream.pipe(flat(2))` - Flatten N levels
|
|
444
|
+
- **Event Multiplication**: 1 array event → N individual events
|
|
333
445
|
|
|
334
446
|
### Reactive Collections
|
|
335
447
|
|
|
@@ -351,111 +463,22 @@ Functional transformers for use with `pipe()` - all support async operations:
|
|
|
351
463
|
- `delete: Stream<T>` - Delete events
|
|
352
464
|
- `clear: Stream<void>` - Clear events
|
|
353
465
|
|
|
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
466
|
## Performance
|
|
443
467
|
|
|
444
|
-
|
|
468
|
+
### Bundle Size
|
|
445
469
|
|
|
446
|
-
- **
|
|
447
|
-
- **
|
|
448
|
-
- **
|
|
449
|
-
- **
|
|
470
|
+
- **Runtime bundle** - 5.5KB minified, 1.6KB gzipped
|
|
471
|
+
- **Package size** - Starts small, grows with JSDoc transformer library
|
|
472
|
+
- **Your production app** - Always gets only the 5.5KB runtime code
|
|
473
|
+
- **Tree-shakeable** - Import only what you use
|
|
450
474
|
|
|
451
|
-
###
|
|
475
|
+
### Benchmarks
|
|
452
476
|
|
|
453
|
-
- **
|
|
454
|
-
- **Efficient
|
|
455
|
-
- **
|
|
456
|
-
- **Automatic cleanup** prevents memory leaks
|
|
477
|
+
- **Fast startup** - Zero dependencies, instant initialization
|
|
478
|
+
- **Efficient pipelines** - Optimized transformer composition
|
|
479
|
+
- **Memory bounded** - Built-in backpressure handling
|
|
457
480
|
|
|
458
|
-
##
|
|
481
|
+
## Runtime Support
|
|
459
482
|
|
|
460
483
|
- **Modern browsers** supporting ES2020+
|
|
461
484
|
- **Node.js** 16+
|
|
@@ -465,28 +488,6 @@ unreadCount.listen((count) => {
|
|
|
465
488
|
|
|
466
489
|
## Migration Guide
|
|
467
490
|
|
|
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
491
|
### From EventEmitter
|
|
491
492
|
|
|
492
493
|
```typescript
|
|
@@ -503,6 +504,24 @@ stream.listen(console.log);
|
|
|
503
504
|
stream.push("hello");
|
|
504
505
|
```
|
|
505
506
|
|
|
507
|
+
## Documentation
|
|
508
|
+
|
|
509
|
+
### Transformer Guides
|
|
510
|
+
|
|
511
|
+
- **[Filter Transformer →](src/transformers/filter.md)** - Adaptive constraints and stream termination
|
|
512
|
+
- **[Map Transformer →](src/transformers/map.md)** - Stateful transformations and async processing
|
|
513
|
+
- **[Merge Transformer →](src/transformers/merge.md)** - Stream orchestration and type-safe combination
|
|
514
|
+
- **[Flat Transformer →](src/transformers/flat.md)** - Event multiplication and array flattening
|
|
515
|
+
|
|
516
|
+
### Philosophy
|
|
517
|
+
|
|
518
|
+
**Adaptive Reactive Programming** - A new paradigm where transformers maintain state and evolve their behavior based on stream history. This enables:
|
|
519
|
+
|
|
520
|
+
- **Learning transformers** that adapt to data patterns
|
|
521
|
+
- **Stateful operations** with memory between events
|
|
522
|
+
- **Stream termination** for lifecycle control
|
|
523
|
+
- **Zero-overhead types** with perfect inference
|
|
524
|
+
|
|
506
525
|
## Contributing
|
|
507
526
|
|
|
508
527
|
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
@@ -525,5 +544,5 @@ Contact: <smari.sofiane@gmail.com>
|
|
|
525
544
|
---
|
|
526
545
|
|
|
527
546
|
<div align="center">
|
|
528
|
-
<strong>
|
|
547
|
+
<strong>Pioneering Adaptive Reactive Programming</strong>
|
|
529
548
|
</div>
|