@soffinal/stream 0.1.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/LICENSE +21 -0
- package/README.md +776 -0
- package/dist/benchmark.d.ts +16 -0
- package/dist/benchmark.d.ts.map +1 -0
- package/dist/benchmark.js +151 -0
- package/dist/benchmark.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/list.d.ts +153 -0
- package/dist/list.d.ts.map +1 -0
- package/dist/list.js +200 -0
- package/dist/list.js.map +1 -0
- package/dist/map.d.ts +97 -0
- package/dist/map.d.ts.map +1 -0
- package/dist/map.js +99 -0
- package/dist/map.js.map +1 -0
- package/dist/set.d.ts +92 -0
- package/dist/set.d.ts.map +1 -0
- package/dist/set.js +94 -0
- package/dist/set.js.map +1 -0
- package/dist/state.d.ts +79 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +90 -0
- package/dist/state.js.map +1 -0
- package/dist/stream.d.ts +480 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +558 -0
- package/dist/stream.js.map +1 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
# @soffinal/stream
|
|
2
|
+
|
|
3
|
+
Reactive async-first streaming library with functional transformations and reactive data structures. Build real-time apps with streams, state management, and event-driven collections.
|
|
4
|
+
|
|
5
|
+
## Key Features
|
|
6
|
+
|
|
7
|
+
- **Multicast by Default**: One stream, many consumers - perfect for event systems
|
|
8
|
+
- **Awaitable Streams**: Simply `await stream` to get the next value
|
|
9
|
+
- **Async Iterable**: Use `for await` loops to process data as it flows
|
|
10
|
+
- **Lazy & Shared**: All operation run once per event, results shared across all listeners
|
|
11
|
+
- **Auto Cleanup**: Resources freed automatically when no longer needed
|
|
12
|
+
- **Async & Stateful Transformers**: Filter, map, group accept sync/async predicates and can maintain state
|
|
13
|
+
- **Custom Transformers**: Build your own transformers like throttle, debounce, distinctUntilChanged
|
|
14
|
+
- **Reactive State**: State objects that automatically notify dependents of changes
|
|
15
|
+
- **Reactive Collections**: Lists, Maps, Sets that emit change events
|
|
16
|
+
|
|
17
|
+
**@soffinal/stream** treats asynchronous data flow as the primary concern, making it useful for:
|
|
18
|
+
|
|
19
|
+
- Real-time data processing
|
|
20
|
+
- Event-driven architectures
|
|
21
|
+
- Streaming APIs and WebSockets
|
|
22
|
+
- State management
|
|
23
|
+
- Memory-efficient data pipelines
|
|
24
|
+
- UI updates
|
|
25
|
+
|
|
26
|
+
## The Stream: Your Data Pipeline Foundation
|
|
27
|
+
|
|
28
|
+
A `Stream` is an async iterable that can push values to multiple listeners, while also being a promise for the next value. Think of it as a data pipe where information flows through - multiple consumers can tap into the same flow to receive all values, or you can simply await the stream to get the next single value.
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { Stream } from "@soffinal/stream";
|
|
32
|
+
|
|
33
|
+
// Create a data pipeline
|
|
34
|
+
const userEvents = new Stream<{ userId: string; action: string }>();
|
|
35
|
+
|
|
36
|
+
// Multiple consumers can listen to the same stream
|
|
37
|
+
userEvents.listen((event) => logToAnalytics(event));
|
|
38
|
+
userEvents.listen((event) => updateUserActivity(event));
|
|
39
|
+
userEvents.listen((event) => triggerNotifications(event));
|
|
40
|
+
|
|
41
|
+
// Push data through the pipeline
|
|
42
|
+
userEvents.push({ userId: "alice", action: "login" }, { userId: "bob", action: "purchase" });
|
|
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:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
const messageStream = new Stream<string>();
|
|
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
|
+
})();
|
|
64
|
+
|
|
65
|
+
messageStream.push("user-login", "data-sync", "shutdown");
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Generator-based Streams: Infinite Data Sources
|
|
69
|
+
|
|
70
|
+
Create infinite, lazy data sources using async generators:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
const sensorStream = new Stream(async function* () {
|
|
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
|
+
});
|
|
81
|
+
|
|
82
|
+
sensorStream.listen((data) => console.log("Sensor reading:", data));
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Instance Transformers
|
|
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
|
+
);
|
|
107
|
+
|
|
108
|
+
// Async filtering - validate against external service
|
|
109
|
+
const authorizedEvents = events.filter(async (event) => {
|
|
110
|
+
const isAuthorized = await checkUserPermissions(event.userId);
|
|
111
|
+
return isAuthorized;
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### Map: Data Transformation Pipeline
|
|
116
|
+
|
|
117
|
+
Mapping transforms data while maintaining the stream's async nature:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const rawEvents = new Stream<string>();
|
|
121
|
+
|
|
122
|
+
// Simple transformation
|
|
123
|
+
const parsedEvents = rawEvents.map((json) => JSON.parse(json));
|
|
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
|
+
);
|
|
130
|
+
|
|
131
|
+
// Async transformation - enrich with external data
|
|
132
|
+
const enrichedEvents = parsedEvents.map(async (event) => {
|
|
133
|
+
const userProfile = await getUserProfile(event.userId);
|
|
134
|
+
return { ...event, user: userProfile };
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### Merge: Combine Multiple Streams
|
|
139
|
+
|
|
140
|
+
Combine multiple streams into a single output stream:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
const stream1 = new Stream<string>();
|
|
144
|
+
const stream2 = new Stream<string>();
|
|
145
|
+
const merged = stream1.merge(stream2);
|
|
146
|
+
|
|
147
|
+
merged.listen((msg) => console.log("Merged:", msg));
|
|
148
|
+
|
|
149
|
+
stream1.push("from stream1");
|
|
150
|
+
stream2.push("from stream2");
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### Group: Intelligent Batching
|
|
154
|
+
|
|
155
|
+
Grouping allows you to batch data based on complex conditions:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
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
|
|
179
|
+
|
|
180
|
+
Flatten nested arrays in stream values:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
const arrays = new Stream<number[]>();
|
|
184
|
+
const flattened = arrays.flat();
|
|
185
|
+
|
|
186
|
+
flattened.listen((n) => console.log("Flat:", n));
|
|
187
|
+
arrays.push([1, 2], [3, 4]);
|
|
188
|
+
// Output: Flat: 1, Flat: 2, Flat: 3, Flat: 4
|
|
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>();
|
|
201
|
+
|
|
202
|
+
setTimeout(() => {
|
|
203
|
+
// Both resolve when stream.push() is called
|
|
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
|
+
})();
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### Listener Events
|
|
216
|
+
|
|
217
|
+
React to listener lifecycle changes on streams:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
const stream = new Stream<number>();
|
|
221
|
+
|
|
222
|
+
// Listen to listener lifecycle
|
|
223
|
+
stream.listenerAdded.listen(() => console.log("Listener added"));
|
|
224
|
+
stream.listenerRemoved.listen(() => console.log("Listener removed"));
|
|
225
|
+
|
|
226
|
+
const cleanup = stream.listen((value) => console.log(value));
|
|
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
|
|
249
|
+
|
|
250
|
+
result.listen((value) => console.log("Processed:", value));
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Built-in Transformer Functions
|
|
254
|
+
|
|
255
|
+
The library provides irreducible building block transformers that work seamlessly with pipe:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// Import transformer functions
|
|
259
|
+
import { map, filter, group, merge, flat } from "@soffinal/stream";
|
|
260
|
+
|
|
261
|
+
const stream = new Stream<number>();
|
|
262
|
+
|
|
263
|
+
// Simple transformations
|
|
264
|
+
stream
|
|
265
|
+
.pipe(map((n) => n.toString())) // Stream<string>
|
|
266
|
+
.pipe(filter((s) => s.length > 1)) // Stream<string>
|
|
267
|
+
.pipe(map((s) => parseInt(s))); // Stream<number>
|
|
268
|
+
|
|
269
|
+
// Stateful transformations
|
|
270
|
+
stream
|
|
271
|
+
.pipe(filter(0, (prev, curr) => [curr > prev, Math.max(prev, curr)]))
|
|
272
|
+
.pipe(map(0, (sum, n) => [sum + n, sum + n]));
|
|
273
|
+
|
|
274
|
+
// Advanced transformations
|
|
275
|
+
const stream2 = new Stream<number>();
|
|
276
|
+
stream
|
|
277
|
+
.pipe(merge(stream2)) // Merge multiple streams
|
|
278
|
+
.pipe(group((batch) => batch.length >= 10)) // Batch processing
|
|
279
|
+
.pipe(flat()); // Flatten results
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Custom Transformers: Building Your Own
|
|
283
|
+
|
|
284
|
+
You can create custom transformers using generator functions:
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// DistinctUntilChanged transformer - only emit when value changes
|
|
288
|
+
const distinctUntilChanged = <T>(stream: Stream<T>): Stream<T> => {
|
|
289
|
+
return new Stream<T>(async function* () {
|
|
290
|
+
let prev: T;
|
|
291
|
+
let first = true;
|
|
292
|
+
|
|
293
|
+
for await (const value of stream) {
|
|
294
|
+
if (first || value !== prev) {
|
|
295
|
+
yield value;
|
|
296
|
+
prev = value;
|
|
297
|
+
first = false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Rate limiting transformer
|
|
304
|
+
const throttle =
|
|
305
|
+
<T>(ms: number) =>
|
|
306
|
+
(stream: Stream<T>): Stream<T> => {
|
|
307
|
+
return new Stream<T>(async function* () {
|
|
308
|
+
let lastEmit = 0;
|
|
309
|
+
|
|
310
|
+
for await (const value of stream) {
|
|
311
|
+
const now = Date.now();
|
|
312
|
+
if (now - lastEmit >= ms) {
|
|
313
|
+
yield value;
|
|
314
|
+
lastEmit = now;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// Usage with perfect type inference
|
|
321
|
+
const searchInput = new Stream<string>();
|
|
322
|
+
const searchResults = searchInput
|
|
323
|
+
.pipe(distinctUntilChanged) // Only emit when search term changes
|
|
324
|
+
.pipe(throttle(1000)) // Limit API calls
|
|
325
|
+
.pipe(map((query) => searchAPI(query)));
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Dual Programming Paradigms
|
|
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
|
|
364
|
+
|
|
365
|
+
State objects are streams that hold current values, enabling reactive programming patterns:
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
import { State } from "@soffinal/stream";
|
|
369
|
+
|
|
370
|
+
// Application state
|
|
371
|
+
const user = new State<User | null>(null);
|
|
372
|
+
const theme = new State<"light" | "dark">("light");
|
|
373
|
+
const notifications = new State<Notification[]>([]);
|
|
374
|
+
|
|
375
|
+
// Reactive computations
|
|
376
|
+
const isLoggedIn = user.map((u) => u !== null);
|
|
377
|
+
const unreadCount = notifications.map((n) => n.filter((x) => !x.read).length);
|
|
378
|
+
|
|
379
|
+
// Automatic UI updates
|
|
380
|
+
isLoggedIn.listen((loggedIn) => {
|
|
381
|
+
document.body.classList.toggle("logged-in", loggedIn);
|
|
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();
|
|
453
|
+
});
|
|
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
|
+
```
|
|
469
|
+
|
|
470
|
+
### Map: Reactive Key-Value Store
|
|
471
|
+
|
|
472
|
+
Key-value store that emits events on changes:
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
import { Map } from "@soffinal/stream";
|
|
476
|
+
|
|
477
|
+
const cache = new Map<string, any>();
|
|
478
|
+
|
|
479
|
+
// Listen to cache updates (only emits on actual changes)
|
|
480
|
+
cache.set.listen(([key, value]) => {
|
|
481
|
+
console.log(`Cache updated: ${key} = ${value}`);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Listen to cache evictions
|
|
485
|
+
cache.delete.listen(([key, value]) => {
|
|
486
|
+
console.log(`Cache evicted: ${key}`);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// Listen to clear events
|
|
490
|
+
cache.clear.listen(() => console.log("Cache cleared"));
|
|
491
|
+
|
|
492
|
+
cache.set("user:123", { name: "John" }); // Cache updated: user:123 = {...}
|
|
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)
|
|
496
|
+
|
|
497
|
+
// All native Map methods available
|
|
498
|
+
console.log(cache.size);
|
|
499
|
+
console.log(cache.has("key"));
|
|
500
|
+
for (const [key, value] of cache) {
|
|
501
|
+
console.log(key, value);
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Set: Reactive Unique Collections
|
|
506
|
+
|
|
507
|
+
Unique value collection that emits events on changes:
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
import { Set } from "@soffinal/stream";
|
|
511
|
+
|
|
512
|
+
const activeUsers = new Set<string>();
|
|
513
|
+
|
|
514
|
+
// Listen to additions (only emits for new values)
|
|
515
|
+
activeUsers.add.listen((userId) => {
|
|
516
|
+
console.log(`User ${userId} came online`);
|
|
517
|
+
broadcastUserStatus(userId, "online");
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Listen to deletions
|
|
521
|
+
activeUsers.delete.listen((userId) => {
|
|
522
|
+
console.log(`User ${userId} went offline`);
|
|
523
|
+
broadcastUserStatus(userId, "offline");
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Listen to clear events
|
|
527
|
+
activeUsers.clear.listen(() => console.log("All users cleared"));
|
|
528
|
+
|
|
529
|
+
activeUsers.add("alice"); // User alice came online
|
|
530
|
+
activeUsers.add("alice"); // No emission (duplicate)
|
|
531
|
+
activeUsers.delete("alice"); // User alice went offline
|
|
532
|
+
activeUsers.delete("bob"); // No emission (didn't exist)
|
|
533
|
+
|
|
534
|
+
// All native Set methods available
|
|
535
|
+
console.log(activeUsers.size);
|
|
536
|
+
console.log(activeUsers.has("alice"));
|
|
537
|
+
for (const user of activeUsers) {
|
|
538
|
+
console.log(user);
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
## Use Cases
|
|
543
|
+
|
|
544
|
+
Practical examples of building reactive applications.
|
|
545
|
+
|
|
546
|
+
### Real-time Data Processing
|
|
547
|
+
|
|
548
|
+
Process sensor data with filtering and transformation:
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
const sensorData = new Stream<{ temperature: number; humidity: number }>();
|
|
552
|
+
|
|
553
|
+
// Alert system
|
|
554
|
+
sensorData.filter((data) => data.temperature > 30).listen((data) => console.log("High temperature alert:", data));
|
|
555
|
+
|
|
556
|
+
// Data logging
|
|
557
|
+
sensorData.map((data) => ({ ...data, timestamp: Date.now() })).listen((data) => saveToDatabase(data));
|
|
558
|
+
|
|
559
|
+
// Simulate sensor readings
|
|
560
|
+
setInterval(() => {
|
|
561
|
+
sensorData.push({
|
|
562
|
+
temperature: Math.random() * 40,
|
|
563
|
+
humidity: Math.random() * 100,
|
|
564
|
+
});
|
|
565
|
+
}, 1000);
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Event Aggregation
|
|
569
|
+
|
|
570
|
+
Collect and batch user events for analytics:
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
const userActions = new Stream<{ userId: string; action: string }>();
|
|
574
|
+
|
|
575
|
+
// Group actions by time windows
|
|
576
|
+
const actionBatches = userActions.group(0, (count, action) => {
|
|
577
|
+
return count >= 10 ? [true, 0] : [false, count + 1];
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
actionBatches.listen((count) => {
|
|
581
|
+
console.log(`Processed ${count} actions`);
|
|
582
|
+
});
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### WebSocket Integration
|
|
586
|
+
|
|
587
|
+
Create reactive streams from WebSocket connections:
|
|
588
|
+
|
|
589
|
+
```typescript
|
|
590
|
+
const wsStream = new Stream<MessageEvent>(async function* () {
|
|
591
|
+
const ws = new WebSocket("wss://api.example.com");
|
|
592
|
+
|
|
593
|
+
while (ws.readyState !== WebSocket.CLOSED) {
|
|
594
|
+
yield await new Promise((resolve) => {
|
|
595
|
+
ws.onmessage = resolve;
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
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);
|
|
645
|
+
|
|
646
|
+
// Multiple consumers for different purposes
|
|
647
|
+
processedData.listen((anomalies) => sendAlerts(anomalies));
|
|
648
|
+
processedData.listen((anomalies) => updateDashboard(anomalies));
|
|
649
|
+
processedData.listen((anomalies) => logToDatabase(anomalies));
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
### Cleanup and Memory Management
|
|
653
|
+
|
|
654
|
+
Properly clean up listeners and prevent memory leaks:
|
|
655
|
+
|
|
656
|
+
```typescript
|
|
657
|
+
const stream = new Stream<number>();
|
|
658
|
+
|
|
659
|
+
// Automatic cleanup with AbortSignal
|
|
660
|
+
const controller = new AbortController();
|
|
661
|
+
stream.listen((value) => console.log(value), controller.signal);
|
|
662
|
+
|
|
663
|
+
// Clean up when component unmounts
|
|
664
|
+
setTimeout(() => controller.abort(), 5000);
|
|
665
|
+
|
|
666
|
+
// Or manual cleanup
|
|
667
|
+
const cleanup = stream.listen((value) => console.log(value));
|
|
668
|
+
setTimeout(cleanup, 5000);
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
## Installation
|
|
672
|
+
|
|
673
|
+
```bash
|
|
674
|
+
bun add @soffinal/stream
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
```bash
|
|
678
|
+
npm install @soffinal/stream
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
## API Reference
|
|
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:**
|
|
711
|
+
|
|
712
|
+
- `map<T, U>(mapper: (value: T) => U | Promise<U>): (stream: Stream<T>) => Stream<U>` - Transform values
|
|
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
|
|
721
|
+
|
|
722
|
+
### State<T> extends Stream<T>
|
|
723
|
+
|
|
724
|
+
**Properties:**
|
|
725
|
+
|
|
726
|
+
- `value: T` - Get/set current state value
|
|
727
|
+
|
|
728
|
+
**Methods:**
|
|
729
|
+
|
|
730
|
+
- `push(...values: T[]): void` - Update state with multiple values sequentially
|
|
731
|
+
|
|
732
|
+
### List<T>
|
|
733
|
+
|
|
734
|
+
**Properties:**
|
|
735
|
+
|
|
736
|
+
- `length: number` - Current list length
|
|
737
|
+
- `insert: Stream<[number, T]>` - Emits on insertions
|
|
738
|
+
- `delete: Stream<[number, T]>` - Emits on deletions
|
|
739
|
+
- `clear: Stream<void>` - Emits on clear
|
|
740
|
+
- `[index]: T` - Index access with modulo wrapping
|
|
741
|
+
|
|
742
|
+
**Methods:**
|
|
743
|
+
|
|
744
|
+
- `insert(index: number, value: T): void` - Insert value at index
|
|
745
|
+
- `delete(index: number): T | undefined` - Delete value at index, returns deleted value
|
|
746
|
+
- `clear(): void` - Clear all items
|
|
747
|
+
- `get(index: number): T | undefined` - Get value without modulo wrapping
|
|
748
|
+
- `values(): IterableIterator<T>` - Iterator for values
|
|
749
|
+
|
|
750
|
+
### Map<K,V> extends globalThis.Map<K,V>
|
|
751
|
+
|
|
752
|
+
**Stream Properties:**
|
|
753
|
+
|
|
754
|
+
- `set: Stream<[K, V]>` - Emits on set operation (only actual changes)
|
|
755
|
+
- `delete: Stream<[K, V]>` - Emits on successful deletions
|
|
756
|
+
- `clear: Stream<void>` - Emits on clear (only if not empty)
|
|
757
|
+
|
|
758
|
+
**Methods:**
|
|
759
|
+
|
|
760
|
+
- All native Map methods plus reactive stream properties
|
|
761
|
+
|
|
762
|
+
### Set<T> extends globalThis.Set<T>
|
|
763
|
+
|
|
764
|
+
**Stream Properties:**
|
|
765
|
+
|
|
766
|
+
- `add: Stream<T>` - Emits on successful additions (no duplicates)
|
|
767
|
+
- `delete: Stream<T>` - Emits on successful deletions
|
|
768
|
+
- `clear: Stream<void>` - Emits on clear (only if not empty)
|
|
769
|
+
|
|
770
|
+
**Methods:**
|
|
771
|
+
|
|
772
|
+
- All native Set methods plus reactive stream properties
|
|
773
|
+
|
|
774
|
+
MIT License
|
|
775
|
+
|
|
776
|
+
Copyright (c) 2024 Soffinal <https://github.com/soffinal> <smari.sofiane@gmail.com>
|