@soffinal/stream 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +311 -594
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -6
- package/dist/index.js.map +14 -1
- package/dist/list.d.ts +3 -3
- package/dist/list.d.ts.map +1 -1
- package/dist/map.d.ts +1 -1
- package/dist/map.d.ts.map +1 -1
- package/dist/set.d.ts +1 -1
- package/dist/set.d.ts.map +1 -1
- package/dist/state.d.ts +1 -1
- package/dist/state.d.ts.map +1 -1
- package/dist/stream.d.ts +4 -0
- package/dist/stream.d.ts.map +1 -1
- package/package.json +4 -4
- package/dist/benchmark.js +0 -151
- package/dist/benchmark.js.map +0 -1
- package/dist/list.js +0 -200
- package/dist/list.js.map +0 -1
- package/dist/map.js +0 -99
- package/dist/map.js.map +0 -1
- package/dist/set.js +0 -94
- package/dist/set.js.map +0 -1
- package/dist/state.js +0 -90
- package/dist/state.js.map +0 -1
- package/dist/stream.js +0 -558
- package/dist/stream.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,290 +1,183 @@
|
|
|
1
1
|
# @soffinal/stream
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
##
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// All three listeners receive both events
|
|
44
|
-
|
|
45
|
-
// Stream is also a promise for the next value
|
|
46
|
-
const nextEvent = await userEvents; // Waits for next pushed value
|
|
47
|
-
console.log("Next event:", nextEvent);
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### Async Iteration: Processing Data as it Flows
|
|
51
|
-
|
|
52
|
-
Streams implement `AsyncIterable`, allowing you to process data as it arrives using `for await` loops:
|
|
3
|
+
[](https://badge.fury.io/js/@soffinal%2Fstream)
|
|
4
|
+
[](https://www.typescriptlang.org/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://bundlephobia.com/package/@soffinal/stream)
|
|
7
|
+
|
|
8
|
+
> **High-performance reactive streaming library for TypeScript/JavaScript**
|
|
9
|
+
|
|
10
|
+
A modern, async-first streaming library that treats asynchronous data flow as a first-class citizen. Built for real-time applications, event-driven architectures, and reactive programming patterns.
|
|
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
|
+
- [Browser Support](#browser-support)
|
|
22
|
+
- [Migration Guide](#migration-guide)
|
|
23
|
+
- [Contributing](#contributing)
|
|
24
|
+
- [License](#license)
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- 🚀 **High Performance** - Optimized for speed and efficiency
|
|
29
|
+
- 🔄 **Multicast by Default** - One stream, many consumers
|
|
30
|
+
- ⏳ **Awaitable Streams** - `await stream` for next value
|
|
31
|
+
- 🔁 **Async Iterable** - Use `for await` loops naturally
|
|
32
|
+
- 🧠 **Smart Caching** - Operations run once, results shared
|
|
33
|
+
- 🗑️ **Auto Cleanup** - Memory management handled automatically
|
|
34
|
+
- 🔧 **Dual Programming Paradigms** - Method chaining or functional pipe composition
|
|
35
|
+
- 🛠️ **Custom Transformers** - Create reusable transformers in both OOP and functional styles
|
|
36
|
+
- 📊 **Reactive Collections** - Lists, Maps, Sets with fine-grained change events
|
|
37
|
+
- 🔀 **Stateful Operations** - Built-in support for stateful filtering, mapping, and grouping
|
|
38
|
+
- 📦 **Zero Dependencies** - Lightweight (~6KB minified, ~2KB gzipped) and tree-shakeable
|
|
39
|
+
- 🌐 **Universal** - Node.js, browsers, Deno, Bun, Cloudflare Workers
|
|
40
|
+
- 📘 **Full TypeScript** - Complete type safety and inference
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
53
43
|
|
|
54
44
|
```typescript
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// Process messages as they arrive
|
|
58
|
-
(async () => {
|
|
59
|
-
for await (const message of messageStream) {
|
|
60
|
-
await processMessage(message);
|
|
61
|
-
if (message === "shutdown") break;
|
|
62
|
-
}
|
|
63
|
-
})();
|
|
45
|
+
import { Stream, State } from "@soffinal/stream";
|
|
64
46
|
|
|
65
|
-
|
|
66
|
-
|
|
47
|
+
// Create reactive data streams
|
|
48
|
+
const events = new Stream<string>();
|
|
49
|
+
const counter = new State(0);
|
|
67
50
|
|
|
68
|
-
|
|
51
|
+
// Transform and filter data
|
|
52
|
+
const processed = events.filter((msg) => msg.length > 3).map((msg) => msg.toUpperCase());
|
|
69
53
|
|
|
70
|
-
|
|
54
|
+
// Multiple consumers
|
|
55
|
+
processed.listen((msg) => console.log("Logger:", msg));
|
|
56
|
+
processed.listen((msg) => updateUI(msg));
|
|
71
57
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
while (true) {
|
|
75
|
-
const temperature = await readTemperatureSensor();
|
|
76
|
-
const humidity = await readHumiditySensor();
|
|
77
|
-
yield { temperature, humidity, timestamp: Date.now() };
|
|
78
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
79
|
-
}
|
|
80
|
-
});
|
|
58
|
+
// Push data through the pipeline
|
|
59
|
+
events.push("hello", "hi", "world"); // Only 'HELLO' and 'WORLD' processed
|
|
81
60
|
|
|
82
|
-
|
|
61
|
+
// Reactive state management
|
|
62
|
+
counter.listen((count) => (document.title = `Count: ${count}`));
|
|
63
|
+
counter.value++; // UI updates automatically
|
|
83
64
|
```
|
|
84
65
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
Streams support method chaining for familiar object-oriented programming patterns:
|
|
88
|
-
|
|
89
|
-
#### Filter: Smart Data Selection
|
|
90
|
-
|
|
91
|
-
Filtering goes beyond simple predicates - it supports stateful filtering and async predicates:
|
|
92
|
-
|
|
93
|
-
```typescript
|
|
94
|
-
const events = new Stream<{ type: string; userId: string; data: any }>();
|
|
95
|
-
|
|
96
|
-
// Simple filtering
|
|
97
|
-
const loginEvents = events.filter((e) => e.type === "login");
|
|
98
|
-
|
|
99
|
-
// Stateful filtering - only allow increasing user IDs
|
|
100
|
-
const validEvents = events.filter(
|
|
101
|
-
0, // initial state
|
|
102
|
-
(lastUserId, event) => {
|
|
103
|
-
const currentId = parseInt(event.userId);
|
|
104
|
-
return [currentId > lastUserId, currentId];
|
|
105
|
-
}
|
|
106
|
-
);
|
|
66
|
+
## Installation
|
|
107
67
|
|
|
108
|
-
|
|
109
|
-
const authorizedEvents = events.filter(async (event) => {
|
|
110
|
-
const isAuthorized = await checkUserPermissions(event.userId);
|
|
111
|
-
return isAuthorized;
|
|
112
|
-
});
|
|
113
|
-
```
|
|
68
|
+
### Package Managers
|
|
114
69
|
|
|
115
|
-
|
|
70
|
+
```bash
|
|
71
|
+
# npm
|
|
72
|
+
npm install @soffinal/stream
|
|
116
73
|
|
|
117
|
-
|
|
74
|
+
# yarn
|
|
75
|
+
yarn add @soffinal/stream
|
|
118
76
|
|
|
119
|
-
|
|
120
|
-
|
|
77
|
+
# pnpm
|
|
78
|
+
pnpm add @soffinal/stream
|
|
121
79
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// Stateful transformation - add sequence numbers
|
|
126
|
-
const sequencedEvents = rawEvents.map(
|
|
127
|
-
0, // initial sequence
|
|
128
|
-
(seq, event) => [{ ...event, sequence: seq + 1 }, seq + 1]
|
|
129
|
-
);
|
|
80
|
+
# bun
|
|
81
|
+
bun add @soffinal/stream
|
|
130
82
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const userProfile = await getUserProfile(event.userId);
|
|
134
|
-
return { ...event, user: userProfile };
|
|
135
|
-
});
|
|
83
|
+
# Deno
|
|
84
|
+
deno add jsr:@soffinal/stream
|
|
136
85
|
```
|
|
137
86
|
|
|
138
|
-
|
|
87
|
+
### CDN (Browser)
|
|
139
88
|
|
|
140
|
-
|
|
89
|
+
```html
|
|
90
|
+
<!-- Production (minified) -->
|
|
91
|
+
<script type="module">
|
|
92
|
+
import { Stream, State } from "https://cdn.jsdelivr.net/npm/@soffinal/stream@latest/dist/index.js";
|
|
93
|
+
</script>
|
|
141
94
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
merged.listen((msg) => console.log("Merged:", msg));
|
|
148
|
-
|
|
149
|
-
stream1.push("from stream1");
|
|
150
|
-
stream2.push("from stream2");
|
|
95
|
+
<!-- Alternative CDNs -->
|
|
96
|
+
<script type="module">
|
|
97
|
+
import { Stream } from "https://esm.sh/@soffinal/stream";
|
|
98
|
+
import { Stream } from "https://cdn.skypack.dev/@soffinal/stream";
|
|
99
|
+
</script>
|
|
151
100
|
```
|
|
152
101
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
Grouping allows you to batch data based on complex conditions:
|
|
102
|
+
## Core Concepts
|
|
156
103
|
|
|
157
|
-
|
|
158
|
-
const transactions = new Stream<{ amount: number; userId: string }>();
|
|
159
|
-
|
|
160
|
-
// Batch by count
|
|
161
|
-
const countBatches = transactions.group((batch) => batch.length >= 100);
|
|
162
|
-
|
|
163
|
-
// Batch by total amount
|
|
164
|
-
const amountBatches = transactions.group(
|
|
165
|
-
0, // initial sum
|
|
166
|
-
(sum, transaction) => {
|
|
167
|
-
const newSum = sum + transaction.amount;
|
|
168
|
-
return [newSum >= 10000, newSum >= 10000 ? 0 : newSum];
|
|
169
|
-
}
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
// Process batches efficiently
|
|
173
|
-
amountBatches.listen(async (totalAmount) => {
|
|
174
|
-
await processBulkTransaction(totalAmount);
|
|
175
|
-
});
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
#### Flat: Flatten Nested Arrays
|
|
104
|
+
### Streams: Async Data Pipelines
|
|
179
105
|
|
|
180
|
-
|
|
106
|
+
A `Stream` is an async iterable that can push values to multiple listeners while also being awaitable for the next value.
|
|
181
107
|
|
|
182
108
|
```typescript
|
|
183
|
-
const
|
|
184
|
-
const flattened = arrays.flat();
|
|
109
|
+
const userEvents = new Stream<UserEvent>();
|
|
185
110
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// Deep flattening
|
|
191
|
-
const nested = new Stream<number[][][]>();
|
|
192
|
-
const deepFlat = nested.flat(2);
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
#### Promise Interface
|
|
196
|
-
|
|
197
|
-
Streams are directly awaitable for the next event/data:
|
|
198
|
-
|
|
199
|
-
```typescript
|
|
200
|
-
const stream = new Stream<number>();
|
|
111
|
+
// Multiple consumers
|
|
112
|
+
userEvents.listen((event) => analytics.track(event));
|
|
113
|
+
userEvents.listen((event) => notifications.send(event));
|
|
114
|
+
userEvents.listen((event) => database.save(event));
|
|
201
115
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
stream.push(5); // nextValue = 5, doubled = 10
|
|
205
|
-
})(async () => {
|
|
206
|
-
// Simply await the stream for the next value
|
|
207
|
-
const nextValue = await stream;
|
|
208
|
-
console.log("Received:", nextValue);
|
|
209
|
-
})()(async () => {
|
|
210
|
-
// Or use .then() for transformation
|
|
211
|
-
const doubled = await stream.then((x) => x * 2);
|
|
212
|
-
})();
|
|
116
|
+
// Or await the next event
|
|
117
|
+
const nextEvent = await userEvents;
|
|
213
118
|
```
|
|
214
119
|
|
|
215
|
-
|
|
120
|
+
### Transformers: Fluent & Functional Styles
|
|
216
121
|
|
|
217
|
-
|
|
122
|
+
#### Method Chaining (Fluent)
|
|
218
123
|
|
|
219
124
|
```typescript
|
|
220
125
|
const stream = new Stream<number>();
|
|
221
126
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
cleanup(); // Triggers 'Listener removed'
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
## Functional Programming style: Composable Data Transformations
|
|
231
|
-
|
|
232
|
-
The library provides functional programming patterns through the `pipe` method and transformer functions, enabling powerful composition and reusable transformation pipelines.
|
|
233
|
-
|
|
234
|
-
### Pipe: Functional Composition Made Easy
|
|
235
|
-
|
|
236
|
-
The `pipe` method enables functional composition of stream transformations:
|
|
237
|
-
|
|
238
|
-
```typescript
|
|
239
|
-
import { Stream, map, filter, group, merge, flat } from "@soffinal/stream";
|
|
240
|
-
|
|
241
|
-
const numbers = new Stream<number>();
|
|
242
|
-
|
|
243
|
-
// Functional composition with built-in transformers
|
|
244
|
-
const result = numbers
|
|
245
|
-
.pipe(filter((n) => n > 0)) // Remove negative numbers
|
|
246
|
-
.pipe(map((n) => n * 2)) // Double each value
|
|
247
|
-
.pipe(group((batch) => batch.length >= 5)) // Group into batches of 5
|
|
248
|
-
.pipe(flat()); // Flatten the batches
|
|
127
|
+
// Simple transformations
|
|
128
|
+
const result = stream
|
|
129
|
+
.filter((x) => x > 0)
|
|
130
|
+
.map((x) => x * 2)
|
|
131
|
+
.group((batch) => batch.length >= 5);
|
|
249
132
|
|
|
250
|
-
|
|
133
|
+
// Stateful operations with initial state
|
|
134
|
+
const stateful = stream
|
|
135
|
+
.filter(0, (prev, curr) => [curr > prev, Math.max(prev, curr)]) // Only increasing values
|
|
136
|
+
.map([], (history, value) => {
|
|
137
|
+
const newHistory = [...history, value].slice(-3); // Keep last 3
|
|
138
|
+
const average = newHistory.reduce((a, b) => a + b, 0) / newHistory.length;
|
|
139
|
+
return [{ value, average, history: newHistory }, newHistory];
|
|
140
|
+
})
|
|
141
|
+
.group(5, (count, item) => [count >= 5, count >= 5 ? 0 : count + 1]); // Batch every 5
|
|
251
142
|
```
|
|
252
143
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
The library provides irreducible building block transformers that work seamlessly with pipe:
|
|
144
|
+
#### Functional Composition (Pipe)
|
|
256
145
|
|
|
257
146
|
```typescript
|
|
258
|
-
|
|
259
|
-
import { map, filter, group, merge, flat } from "@soffinal/stream";
|
|
260
|
-
|
|
261
|
-
const stream = new Stream<number>();
|
|
147
|
+
import { filter, map, group, merge } from "@soffinal/stream";
|
|
262
148
|
|
|
263
|
-
// Simple
|
|
264
|
-
stream
|
|
265
|
-
.pipe(
|
|
266
|
-
.pipe(
|
|
267
|
-
.pipe(
|
|
149
|
+
// Simple functional composition
|
|
150
|
+
const result = stream
|
|
151
|
+
.pipe(filter((x) => x > 0))
|
|
152
|
+
.pipe(map((x) => x * 2))
|
|
153
|
+
.pipe(group((batch) => batch.length >= 5));
|
|
268
154
|
|
|
269
|
-
// Stateful
|
|
270
|
-
stream
|
|
155
|
+
// Stateful functional transformers
|
|
156
|
+
const advanced = stream
|
|
271
157
|
.pipe(filter(0, (prev, curr) => [curr > prev, Math.max(prev, curr)]))
|
|
272
|
-
.pipe(
|
|
158
|
+
.pipe(
|
|
159
|
+
map([], (acc, value) => {
|
|
160
|
+
const newAcc = [...acc, value].slice(-10);
|
|
161
|
+
const sum = newAcc.reduce((a, b) => a + b, 0);
|
|
162
|
+
return [{ value, sum, count: newAcc.length }, newAcc];
|
|
163
|
+
})
|
|
164
|
+
)
|
|
165
|
+
.pipe(group(0, (count, item) => [count >= 3, count >= 3 ? 0 : count + 1]));
|
|
273
166
|
|
|
274
|
-
//
|
|
167
|
+
// Combining multiple streams
|
|
275
168
|
const stream2 = new Stream<number>();
|
|
276
|
-
stream
|
|
277
|
-
.pipe(
|
|
278
|
-
.pipe(
|
|
279
|
-
.pipe(
|
|
169
|
+
const merged = stream
|
|
170
|
+
.pipe(filter((x) => x % 2 === 0))
|
|
171
|
+
.pipe(merge(stream2))
|
|
172
|
+
.pipe(map((x) => x.toString()));
|
|
280
173
|
```
|
|
281
174
|
|
|
282
|
-
### Custom Transformers
|
|
175
|
+
### Custom Transformers
|
|
283
176
|
|
|
284
|
-
|
|
177
|
+
Create reusable transformers for both paradigms:
|
|
285
178
|
|
|
286
179
|
```typescript
|
|
287
|
-
//
|
|
180
|
+
// Custom transformer: distinctUntilChanged
|
|
288
181
|
const distinctUntilChanged = <T>(stream: Stream<T>): Stream<T> => {
|
|
289
182
|
return new Stream<T>(async function* () {
|
|
290
183
|
let prev: T;
|
|
@@ -300,7 +193,7 @@ const distinctUntilChanged = <T>(stream: Stream<T>): Stream<T> => {
|
|
|
300
193
|
});
|
|
301
194
|
};
|
|
302
195
|
|
|
303
|
-
//
|
|
196
|
+
// Custom transformer: throttle
|
|
304
197
|
const throttle =
|
|
305
198
|
<T>(ms: number) =>
|
|
306
199
|
(stream: Stream<T>): Stream<T> => {
|
|
@@ -320,272 +213,126 @@ const throttle =
|
|
|
320
213
|
// Usage with perfect type inference
|
|
321
214
|
const searchInput = new Stream<string>();
|
|
322
215
|
const searchResults = searchInput
|
|
323
|
-
.pipe(distinctUntilChanged)
|
|
324
|
-
.pipe(throttle(
|
|
216
|
+
.pipe(distinctUntilChanged)
|
|
217
|
+
.pipe(throttle(300))
|
|
325
218
|
.pipe(map((query) => searchAPI(query)));
|
|
326
219
|
```
|
|
327
220
|
|
|
328
|
-
###
|
|
329
|
-
|
|
330
|
-
@soffinal/stream supports both Object-Oriented and Functional programming styles:
|
|
331
|
-
|
|
332
|
-
#### Object-Oriented Style (Method Chaining)
|
|
333
|
-
|
|
334
|
-
```typescript
|
|
335
|
-
// Familiar method chaining
|
|
336
|
-
const result = stream
|
|
337
|
-
.filter((x) => x > 0)
|
|
338
|
-
.map((x) => x * 2)
|
|
339
|
-
.group((batch) => batch.length >= 5);
|
|
340
|
-
|
|
341
|
-
// Rich instance methods with overloads
|
|
342
|
-
stream
|
|
343
|
-
.filter(0, (prev, curr) => [curr > prev, curr]) // Stateful filtering
|
|
344
|
-
.map(async (x) => await enrich(x)); // Async mapping
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
#### Functional Style (Pipe Composition)
|
|
348
|
-
|
|
349
|
-
```typescript
|
|
350
|
-
// Composable transformers
|
|
351
|
-
const result = stream
|
|
352
|
-
.pipe(filter((x) => x > 0))
|
|
353
|
-
.pipe(map((x) => x * 2))
|
|
354
|
-
.pipe(group((batch) => batch.length >= 5));
|
|
355
|
-
|
|
356
|
-
// Reusable transformation pipelines
|
|
357
|
-
const processNumbers = (stream: Stream<number>) => stream.pipe(filter((x) => x > 0)).pipe(map((x) => x * 2));
|
|
358
|
-
|
|
359
|
-
const result1 = stream1.pipe(processNumbers);
|
|
360
|
-
const result2 = stream2.pipe(processNumbers);
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
## State: Reactive State Management
|
|
221
|
+
### Reactive State
|
|
364
222
|
|
|
365
|
-
State objects
|
|
223
|
+
State objects hold current values and notify dependents of changes:
|
|
366
224
|
|
|
367
225
|
```typescript
|
|
368
|
-
import { State } from "@soffinal/stream";
|
|
369
|
-
|
|
370
|
-
// Application state
|
|
371
226
|
const user = new State<User | null>(null);
|
|
372
227
|
const theme = new State<"light" | "dark">("light");
|
|
373
|
-
const notifications = new State<Notification[]>([]);
|
|
374
228
|
|
|
375
|
-
//
|
|
229
|
+
// Derived state with transformations
|
|
376
230
|
const isLoggedIn = user.map((u) => u !== null);
|
|
377
|
-
const
|
|
231
|
+
const userProfile = user
|
|
232
|
+
.filter((u): u is User => u !== null)
|
|
233
|
+
.map((u) => ({ ...u, displayName: u.firstName + " " + u.lastName }));
|
|
378
234
|
|
|
379
235
|
// Automatic UI updates
|
|
380
236
|
isLoggedIn.listen((loggedIn) => {
|
|
381
|
-
document.body.classList.toggle("
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
unreadCount.listen((count) => {
|
|
385
|
-
document.title = count > 0 ? `(${count}) My App` : "My App";
|
|
386
|
-
});
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
### State Management Example
|
|
390
|
-
|
|
391
|
-
```typescript
|
|
392
|
-
interface AppState {
|
|
393
|
-
user: { id: string; name: string } | null;
|
|
394
|
-
notifications: string[];
|
|
395
|
-
theme: "light" | "dark";
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const appState = new State<AppState>({
|
|
399
|
-
user: null,
|
|
400
|
-
notifications: [],
|
|
401
|
-
theme: "light",
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
const isLoggedIn = appState.map((state) => state.user !== null);
|
|
405
|
-
const notificationCount = appState.map((state) => state.notifications.length);
|
|
406
|
-
|
|
407
|
-
// React to login state
|
|
408
|
-
isLoggedIn.listen((loggedIn) => {
|
|
409
|
-
if (loggedIn) {
|
|
410
|
-
console.log("User logged in");
|
|
411
|
-
loadUserData();
|
|
412
|
-
}
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
// Update state immutably
|
|
416
|
-
function login(user: { id: string; name: string }) {
|
|
417
|
-
appState.value = {
|
|
418
|
-
...appState.value,
|
|
419
|
-
user,
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function addNotification(message: string) {
|
|
424
|
-
appState.value = {
|
|
425
|
-
...appState.value,
|
|
426
|
-
notifications: [...appState.value.notifications, message],
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
## Reactive Collections: Data Structures That Notify
|
|
432
|
-
|
|
433
|
-
The library extends JavaScript's native collections with reactive capabilities. Every mutation emits events, allowing you to build reactive UIs and data synchronization systems.
|
|
434
|
-
|
|
435
|
-
### List: Reactive Arrays
|
|
436
|
-
|
|
437
|
-
Reactive Lists provide array-like functionality with fine-grained change notifications:
|
|
438
|
-
|
|
439
|
-
```typescript
|
|
440
|
-
import { List } from "@soffinal/stream";
|
|
441
|
-
|
|
442
|
-
// Create a reactive todo list
|
|
443
|
-
const todos = new List<{ id: string; text: string; done: boolean }>();
|
|
444
|
-
|
|
445
|
-
// Build reactive UI components
|
|
446
|
-
todos.insert.listen(([index, todo]) => {
|
|
447
|
-
const element = createTodoElement(todo);
|
|
448
|
-
todoContainer.insertBefore(element, todoContainer.children[index]);
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
todos.delete.listen(([index, todo]) => {
|
|
452
|
-
todoContainer.children[index].remove();
|
|
237
|
+
document.body.classList.toggle("authenticated", loggedIn);
|
|
453
238
|
});
|
|
454
|
-
|
|
455
|
-
// Reactive operations
|
|
456
|
-
const completedCount = new State(0);
|
|
457
|
-
todos.insert.listen(() => updateCompletedCount());
|
|
458
|
-
todos.delete.listen(() => updateCompletedCount());
|
|
459
|
-
|
|
460
|
-
function updateCompletedCount() {
|
|
461
|
-
const completed = [...todos].filter((t) => t.done).length;
|
|
462
|
-
completedCount.value = completed;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Automatic persistence
|
|
466
|
-
todos.insert.listen(() => saveTodosToStorage([...todos]));
|
|
467
|
-
todos.delete.listen(() => saveTodosToStorage([...todos]));
|
|
468
239
|
```
|
|
469
240
|
|
|
470
|
-
###
|
|
241
|
+
### Reactive Collections
|
|
471
242
|
|
|
472
|
-
|
|
243
|
+
Collections that emit fine-grained change events:
|
|
473
244
|
|
|
474
245
|
```typescript
|
|
475
|
-
|
|
476
|
-
|
|
246
|
+
const todos = new List<Todo>();
|
|
477
247
|
const cache = new Map<string, any>();
|
|
248
|
+
const activeUsers = new Set<string>();
|
|
478
249
|
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
250
|
+
// React to changes
|
|
251
|
+
todos.insert.listen(([index, todo]) => renderTodo(todo, index));
|
|
252
|
+
cache.set.listen(([key, value]) => console.log(`Cached: ${key}`));
|
|
253
|
+
activeUsers.add.listen((userId) => showOnlineStatus(userId));
|
|
254
|
+
```
|
|
483
255
|
|
|
484
|
-
|
|
485
|
-
cache.delete.listen(([key, value]) => {
|
|
486
|
-
console.log(`Cache evicted: ${key}`);
|
|
487
|
-
});
|
|
256
|
+
## API Reference
|
|
488
257
|
|
|
489
|
-
|
|
490
|
-
cache.clear.listen(() => console.log("Cache cleared"));
|
|
258
|
+
### Stream\<T>
|
|
491
259
|
|
|
492
|
-
|
|
493
|
-
cache.set("user:123", { name: "John" }); // No emission (same value)
|
|
494
|
-
cache.delete("user:123"); // Cache evicted: user:123
|
|
495
|
-
cache.delete("nonexistent"); // No emission (didn't exist)
|
|
260
|
+
#### Properties
|
|
496
261
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
for (const [key, value] of cache) {
|
|
501
|
-
console.log(key, value);
|
|
502
|
-
}
|
|
503
|
-
```
|
|
262
|
+
- `hasListeners: boolean` - Whether stream has active listeners
|
|
263
|
+
- `listenerAdded: Stream<void>` - Emits when listener is added
|
|
264
|
+
- `listenerRemoved: Stream<void>` - Emits when listener is removed
|
|
504
265
|
|
|
505
|
-
|
|
266
|
+
#### Methods
|
|
506
267
|
|
|
507
|
-
|
|
268
|
+
- `push(...values: T[]): void` - Emit values to all listeners
|
|
269
|
+
- `listen(callback: (value: T) => void, signal?: AbortSignal): () => void` - Add listener
|
|
270
|
+
- `filter(predicate: (value: T) => boolean): Stream<T>` - Filter values
|
|
271
|
+
- `map<U>(mapper: (value: T) => U): Stream<U>` - Transform values
|
|
272
|
+
- `merge(...streams: Stream<T>[]): Stream<T>` - Combine streams
|
|
273
|
+
- `group(predicate: (batch: T[]) => boolean): Stream<T[]>` - Batch values
|
|
274
|
+
- `flat(depth?: number): Stream<U>` - Flatten arrays
|
|
275
|
+
- `pipe<U>(transformer: (stream: Stream<T>) => Stream<U>): Stream<U>` - Apply transformer
|
|
508
276
|
|
|
509
|
-
|
|
510
|
-
import { Set } from "@soffinal/stream";
|
|
277
|
+
#### Async Interface
|
|
511
278
|
|
|
512
|
-
|
|
279
|
+
- `then<U>(callback?: (value: T) => U): Promise<U>` - Promise interface
|
|
280
|
+
- `[Symbol.asyncIterator](): AsyncIterator<T>` - Async iteration
|
|
513
281
|
|
|
514
|
-
|
|
515
|
-
activeUsers.add.listen((userId) => {
|
|
516
|
-
console.log(`User ${userId} came online`);
|
|
517
|
-
broadcastUserStatus(userId, "online");
|
|
518
|
-
});
|
|
282
|
+
### State\<T> extends Stream\<T>
|
|
519
283
|
|
|
520
|
-
|
|
521
|
-
activeUsers.delete.listen((userId) => {
|
|
522
|
-
console.log(`User ${userId} went offline`);
|
|
523
|
-
broadcastUserStatus(userId, "offline");
|
|
524
|
-
});
|
|
284
|
+
#### Properties
|
|
525
285
|
|
|
526
|
-
|
|
527
|
-
activeUsers.clear.listen(() => console.log("All users cleared"));
|
|
286
|
+
- `value: T` - Current state value (get/set)
|
|
528
287
|
|
|
529
|
-
|
|
530
|
-
activeUsers.add("alice"); // No emission (duplicate)
|
|
531
|
-
activeUsers.delete("alice"); // User alice went offline
|
|
532
|
-
activeUsers.delete("bob"); // No emission (didn't exist)
|
|
288
|
+
### Transformer Functions
|
|
533
289
|
|
|
534
|
-
|
|
535
|
-
console.log(activeUsers.size);
|
|
536
|
-
console.log(activeUsers.has("alice"));
|
|
537
|
-
for (const user of activeUsers) {
|
|
538
|
-
console.log(user);
|
|
539
|
-
}
|
|
540
|
-
```
|
|
290
|
+
Functional transformers for use with `pipe()`:
|
|
541
291
|
|
|
542
|
-
|
|
292
|
+
- `filter<T>(predicate: (value: T) => boolean): (stream: Stream<T>) => Stream<T>`
|
|
293
|
+
- `map<T, U>(mapper: (value: T) => U): (stream: Stream<T>) => Stream<U>`
|
|
294
|
+
- `group<T>(predicate: (batch: T[]) => boolean): (stream: Stream<T>) => Stream<T[]>`
|
|
295
|
+
- `merge<T>(...streams: Stream<T>[]): (stream: Stream<T>) => Stream<T>`
|
|
296
|
+
- `flat<T>(depth?: number): (stream: Stream<T>) => Stream<U>`
|
|
543
297
|
|
|
544
|
-
|
|
298
|
+
### Reactive Collections
|
|
545
299
|
|
|
546
|
-
|
|
300
|
+
#### List\<T>
|
|
547
301
|
|
|
548
|
-
|
|
302
|
+
- `insert: Stream<[number, T]>` - Insertion events
|
|
303
|
+
- `delete: Stream<[number, T]>` - Deletion events
|
|
304
|
+
- `clear: Stream<void>` - Clear events
|
|
549
305
|
|
|
550
|
-
|
|
551
|
-
const sensorData = new Stream<{ temperature: number; humidity: number }>();
|
|
306
|
+
#### Map\<K,V> extends globalThis.Map\<K,V>
|
|
552
307
|
|
|
553
|
-
|
|
554
|
-
|
|
308
|
+
- `set: Stream<[K, V]>` - Set events (only on changes)
|
|
309
|
+
- `delete: Stream<[K, V]>` - Delete events
|
|
310
|
+
- `clear: Stream<void>` - Clear events
|
|
555
311
|
|
|
556
|
-
|
|
557
|
-
sensorData.map((data) => ({ ...data, timestamp: Date.now() })).listen((data) => saveToDatabase(data));
|
|
312
|
+
#### Set\<T> extends globalThis.Set\<T>
|
|
558
313
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
temperature: Math.random() * 40,
|
|
563
|
-
humidity: Math.random() * 100,
|
|
564
|
-
});
|
|
565
|
-
}, 1000);
|
|
566
|
-
```
|
|
314
|
+
- `add: Stream<T>` - Add events (only new values)
|
|
315
|
+
- `delete: Stream<T>` - Delete events
|
|
316
|
+
- `clear: Stream<void>` - Clear events
|
|
567
317
|
|
|
568
|
-
|
|
318
|
+
## Examples
|
|
569
319
|
|
|
570
|
-
|
|
320
|
+
### Real-time Data Processing
|
|
571
321
|
|
|
572
322
|
```typescript
|
|
573
|
-
const
|
|
323
|
+
const sensorData = new Stream<SensorReading>();
|
|
574
324
|
|
|
575
|
-
//
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
})
|
|
325
|
+
// Multi-stage processing pipeline
|
|
326
|
+
const alerts = sensorData
|
|
327
|
+
.filter((reading) => reading.temperature > 30)
|
|
328
|
+
.map((reading) => ({ ...reading, timestamp: Date.now() }))
|
|
329
|
+
.group((batch) => batch.length >= 5);
|
|
579
330
|
|
|
580
|
-
|
|
581
|
-
console.log(`Processed ${count} actions`);
|
|
582
|
-
});
|
|
331
|
+
alerts.listen((batch) => sendAlertNotification(batch));
|
|
583
332
|
```
|
|
584
333
|
|
|
585
334
|
### WebSocket Integration
|
|
586
335
|
|
|
587
|
-
Create reactive streams from WebSocket connections:
|
|
588
|
-
|
|
589
336
|
```typescript
|
|
590
337
|
const wsStream = new Stream<MessageEvent>(async function* () {
|
|
591
338
|
const ws = new WebSocket("wss://api.example.com");
|
|
@@ -597,180 +344,150 @@ const wsStream = new Stream<MessageEvent>(async function* () {
|
|
|
597
344
|
}
|
|
598
345
|
});
|
|
599
346
|
|
|
600
|
-
wsStream
|
|
601
|
-
.map((event) => JSON.parse(event.data))
|
|
602
|
-
.filter((data) => data.type === "update")
|
|
603
|
-
.listen((update) => handleUpdate(update));
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
### Real-time Data Processing Pipeline
|
|
607
|
-
|
|
608
|
-
Build sophisticated data processing systems:
|
|
609
|
-
|
|
610
|
-
```typescript
|
|
611
|
-
// Raw sensor data stream
|
|
612
|
-
const sensorData = new Stream<SensorReading>();
|
|
613
|
-
|
|
614
|
-
// Multi-stage processing pipeline
|
|
615
|
-
const processedData = sensorData
|
|
616
|
-
// 1. Validate data quality
|
|
617
|
-
.filter(async (reading) => {
|
|
618
|
-
return await validateSensorReading(reading);
|
|
619
|
-
})
|
|
620
|
-
// 2. Normalize and enrich
|
|
621
|
-
.map(async (reading) => {
|
|
622
|
-
const location = await getLocationData(reading.sensorId);
|
|
623
|
-
return {
|
|
624
|
-
...reading,
|
|
625
|
-
location,
|
|
626
|
-
normalized: normalizeReading(reading.value),
|
|
627
|
-
};
|
|
628
|
-
})
|
|
629
|
-
// 3. Detect anomalies using sliding window
|
|
630
|
-
.group(
|
|
631
|
-
{ readings: [], sum: 0 }, // sliding window state
|
|
632
|
-
(window, reading) => {
|
|
633
|
-
const newWindow = {
|
|
634
|
-
readings: [...window.readings, reading].slice(-10), // keep last 10
|
|
635
|
-
sum: window.sum + reading.normalized,
|
|
636
|
-
};
|
|
637
|
-
const average = newWindow.sum / newWindow.readings.length;
|
|
638
|
-
const isAnomaly = Math.abs(reading.normalized - average) > threshold;
|
|
639
|
-
|
|
640
|
-
return [isAnomaly, newWindow];
|
|
641
|
-
}
|
|
642
|
-
)
|
|
643
|
-
// 4. Batch anomalies for processing
|
|
644
|
-
.group((batch) => batch.length >= 5 || Date.now() - batch[0]?.timestamp > 30000);
|
|
347
|
+
const messages = wsStream.map((event) => JSON.parse(event.data)).filter((data) => data.type === "update");
|
|
645
348
|
|
|
646
|
-
|
|
647
|
-
processedData.listen((anomalies) => sendAlerts(anomalies));
|
|
648
|
-
processedData.listen((anomalies) => updateDashboard(anomalies));
|
|
649
|
-
processedData.listen((anomalies) => logToDatabase(anomalies));
|
|
349
|
+
messages.listen((update) => handleUpdate(update));
|
|
650
350
|
```
|
|
651
351
|
|
|
652
|
-
###
|
|
653
|
-
|
|
654
|
-
Properly clean up listeners and prevent memory leaks:
|
|
352
|
+
### State Management
|
|
655
353
|
|
|
656
354
|
```typescript
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
// Clean up when component unmounts
|
|
664
|
-
setTimeout(() => controller.abort(), 5000);
|
|
355
|
+
interface AppState {
|
|
356
|
+
user: User | null;
|
|
357
|
+
theme: "light" | "dark";
|
|
358
|
+
notifications: Notification[];
|
|
359
|
+
}
|
|
665
360
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
361
|
+
const appState = new State<AppState>({
|
|
362
|
+
user: null,
|
|
363
|
+
theme: "light",
|
|
364
|
+
notifications: [],
|
|
365
|
+
});
|
|
670
366
|
|
|
671
|
-
|
|
367
|
+
// Derived state
|
|
368
|
+
const unreadCount = appState.map((state) => state.notifications.filter((n) => !n.read).length);
|
|
672
369
|
|
|
673
|
-
|
|
674
|
-
|
|
370
|
+
// Reactive UI
|
|
371
|
+
unreadCount.listen((count) => {
|
|
372
|
+
document.title = count > 0 ? `(${count}) App` : "App";
|
|
373
|
+
});
|
|
675
374
|
```
|
|
676
375
|
|
|
677
|
-
|
|
678
|
-
|
|
376
|
+
### Browser Counter Example
|
|
377
|
+
|
|
378
|
+
```html
|
|
379
|
+
<!DOCTYPE html>
|
|
380
|
+
<html>
|
|
381
|
+
<head>
|
|
382
|
+
<title>Reactive Counter</title>
|
|
383
|
+
</head>
|
|
384
|
+
<body>
|
|
385
|
+
<div id="counter">0</div>
|
|
386
|
+
<button id="increment">+</button>
|
|
387
|
+
<button id="decrement">-</button>
|
|
388
|
+
|
|
389
|
+
<script type="module">
|
|
390
|
+
import { State } from "https://esm.sh/@soffinal/stream";
|
|
391
|
+
|
|
392
|
+
const count = new State(0);
|
|
393
|
+
const display = document.getElementById("counter");
|
|
394
|
+
|
|
395
|
+
// Reactive UI updates
|
|
396
|
+
count.listen((value) => (display.textContent = value));
|
|
397
|
+
|
|
398
|
+
// User interactions
|
|
399
|
+
document.getElementById("increment").onclick = () => count.value++;
|
|
400
|
+
document.getElementById("decrement").onclick = () => count.value--;
|
|
401
|
+
</script>
|
|
402
|
+
</body>
|
|
403
|
+
</html>
|
|
679
404
|
```
|
|
680
405
|
|
|
681
|
-
##
|
|
682
|
-
|
|
683
|
-
### Stream<T>
|
|
684
|
-
|
|
685
|
-
**Properties:**
|
|
686
|
-
|
|
687
|
-
- `hasListeners: boolean` - True if stream has active listeners
|
|
688
|
-
- `listenerAdded: Stream<void>` - Emits when listener is added
|
|
689
|
-
- `listenerRemoved: Stream<void>` - Emits when listener is removed
|
|
690
|
-
|
|
691
|
-
**Methods:**
|
|
692
|
-
|
|
693
|
-
- `push(...values: T[]): void` - Emit values to all listeners
|
|
694
|
-
- `listen(callback: (value: T) => void, signal?: AbortSignal): () => void` - Add listener, returns cleanup function
|
|
695
|
-
- `filter<U extends T>(predicate: (value: T) => value is U): Stream<U>` - Filter values with type guard
|
|
696
|
-
- `filter(predicate: (value: T) => boolean | Promise<boolean>): Stream<T>` - Filter values with predicate
|
|
697
|
-
- `filter<S>(initialState: S, accumulator: (state: S, value: T) => [boolean, S] | Promise<[boolean, S]>): Stream<T>` - Stateful filtering
|
|
698
|
-
- `map<U>(mapper: (value: T) => U | Promise<U>): Stream<U>` - Transform values
|
|
699
|
-
- `map<S, U>(initialState: S, accumulator: (state: S, value: T) => [U, S] | Promise<[U, S]>): Stream<U>` - Stateful mapping
|
|
700
|
-
- `merge(...streams: Stream<T>[]): Stream<T>` - Combine multiple streams
|
|
701
|
-
- `group(predicate: (batch: T[]) => boolean): Stream<T[]>` - Group values into batches
|
|
702
|
-
- `group<S>(initialState: S, accumulator: (state: S, value: T) => [boolean, S]): Stream<T[]>` - Stateful grouping
|
|
703
|
-
- `flat<U>(depth?: number): Stream<U>` - Flatten array values (default depth: 0)
|
|
704
|
-
- `pipe<U>(transformer: (stream: Stream<T>) => Stream<U>): Stream<U>` - Apply functional transformer
|
|
705
|
-
- `then<U>(callback?: (value: T) => U): Promise<U>` - Promise-like interface for first value
|
|
706
|
-
- `[Symbol.asyncIterator](): AsyncIterator<T>` - Async iteration support
|
|
707
|
-
|
|
708
|
-
### Transformer Functions
|
|
709
|
-
|
|
710
|
-
**Built-in transformers for functional composition:**
|
|
406
|
+
## Performance
|
|
711
407
|
|
|
712
|
-
|
|
713
|
-
- `map<T, S, U>(initialState: S, accumulator: (state: S, value: T) => [U, S] | Promise<[U, S]>): (stream: Stream<T>) => Stream<U>` - Stateful mapping
|
|
714
|
-
- `filter<T, U extends T>(predicate: (value: T) => value is U): (stream: Stream<T>) => Stream<U>` - Type guard filtering
|
|
715
|
-
- `filter<T>(predicate: (value: T) => boolean | Promise<boolean>): (stream: Stream<T>) => Stream<T>` - Predicate filtering
|
|
716
|
-
- `filter<T, S>(initialState: S, accumulator: (state: S, value: T) => [boolean, S] | Promise<[boolean, S]>): (stream: Stream<T>) => Stream<T>` - Stateful filtering
|
|
717
|
-
- `group<T>(predicate: (batch: T[]) => boolean): (stream: Stream<T>) => Stream<T[]>` - Group values into batches
|
|
718
|
-
- `group<T, S>(initialState: S, accumulator: (state: S, value: T) => [boolean, S]): (stream: Stream<T>) => Stream<T[]>` - Stateful grouping
|
|
719
|
-
- `merge<T>(...streams: Stream<T>[]): (stream: Stream<T>) => Stream<T>` - Merge multiple streams into one
|
|
720
|
-
- `flat<T, U>(depth?: number): (stream: Stream<T>) => Stream<U>` - Flatten array values with configurable depth
|
|
408
|
+
@soffinal/stream delivers exceptional performance:
|
|
721
409
|
|
|
722
|
-
|
|
410
|
+
- **High throughput** for basic operations
|
|
411
|
+
- **Efficient processing** for complex pipelines
|
|
412
|
+
- **Memory efficient** with automatic cleanup
|
|
413
|
+
- **Zero overhead** for unused features
|
|
723
414
|
|
|
724
|
-
|
|
415
|
+
### Performance Characteristics
|
|
725
416
|
|
|
726
|
-
-
|
|
417
|
+
- **Optimized data structures** for minimal memory allocation
|
|
418
|
+
- **Efficient event dispatch** with smart caching
|
|
419
|
+
- **Lazy evaluation** reduces unnecessary computations
|
|
420
|
+
- **Automatic cleanup** prevents memory leaks
|
|
727
421
|
|
|
728
|
-
|
|
422
|
+
## Browser Support
|
|
729
423
|
|
|
730
|
-
-
|
|
424
|
+
- **Modern browsers** supporting ES2020+
|
|
425
|
+
- **Node.js** 16+
|
|
426
|
+
- **Deno** 1.0+
|
|
427
|
+
- **Bun** 1.0+
|
|
428
|
+
- **Cloudflare Workers**
|
|
731
429
|
|
|
732
|
-
|
|
430
|
+
## Migration Guide
|
|
733
431
|
|
|
734
|
-
|
|
432
|
+
### From RxJS
|
|
735
433
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
434
|
+
```typescript
|
|
435
|
+
// RxJS
|
|
436
|
+
import { Subject, map, filter } from "rxjs";
|
|
437
|
+
const subject = new Subject();
|
|
438
|
+
subject
|
|
439
|
+
.pipe(
|
|
440
|
+
filter((x) => x > 0),
|
|
441
|
+
map((x) => x * 2)
|
|
442
|
+
)
|
|
443
|
+
.subscribe(console.log);
|
|
743
444
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
445
|
+
// @soffinal/stream
|
|
446
|
+
import { Stream } from "@soffinal/stream";
|
|
447
|
+
const stream = new Stream();
|
|
448
|
+
stream
|
|
449
|
+
.filter((x) => x > 0)
|
|
450
|
+
.map((x) => x * 2)
|
|
451
|
+
.listen(console.log);
|
|
452
|
+
```
|
|
749
453
|
|
|
750
|
-
###
|
|
454
|
+
### From EventEmitter
|
|
751
455
|
|
|
752
|
-
|
|
456
|
+
```typescript
|
|
457
|
+
// EventEmitter
|
|
458
|
+
import { EventEmitter } from "events";
|
|
459
|
+
const emitter = new EventEmitter();
|
|
460
|
+
emitter.on("data", console.log);
|
|
461
|
+
emitter.emit("data", "hello");
|
|
753
462
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
463
|
+
// @soffinal/stream
|
|
464
|
+
import { Stream } from "@soffinal/stream";
|
|
465
|
+
const stream = new Stream();
|
|
466
|
+
stream.listen(console.log);
|
|
467
|
+
stream.push("hello");
|
|
468
|
+
```
|
|
757
469
|
|
|
758
|
-
|
|
470
|
+
## Contributing
|
|
759
471
|
|
|
760
|
-
|
|
472
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
761
473
|
|
|
762
|
-
###
|
|
474
|
+
### Development Setup
|
|
763
475
|
|
|
764
|
-
|
|
476
|
+
```bash
|
|
477
|
+
git clone https://github.com/soffinal/stream.git
|
|
478
|
+
cd stream
|
|
479
|
+
bun install
|
|
480
|
+
bun test
|
|
481
|
+
```
|
|
765
482
|
|
|
766
|
-
|
|
767
|
-
- `delete: Stream<T>` - Emits on successful deletions
|
|
768
|
-
- `clear: Stream<void>` - Emits on clear (only if not empty)
|
|
483
|
+
## License
|
|
769
484
|
|
|
770
|
-
|
|
485
|
+
MIT © [Soffinal](https://github.com/soffinal)
|
|
771
486
|
|
|
772
|
-
|
|
487
|
+
Contact: <smari.sofiane@gmail.com>
|
|
773
488
|
|
|
774
|
-
|
|
489
|
+
---
|
|
775
490
|
|
|
776
|
-
|
|
491
|
+
<div align="center">
|
|
492
|
+
<strong>Built with ❤️ for the JavaScript community</strong>
|
|
493
|
+
</div>
|