@soffinal/stream 0.1.2 → 0.1.4

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.
Files changed (2) hide show
  1. package/README.md +340 -621
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,290 +1,201 @@
1
1
  # @soffinal/stream
2
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.
3
+ [![npm version](https://badge.fury.io/js/@soffinal%2Fstream.svg)](https://badge.fury.io/js/@soffinal%2Fstream)
4
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/@soffinal/stream)](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
+ - ⚡ **Async Operations** - All transformers support async functions while maintaining order
33
+ - 🧠 **Smart Caching** - Operations run once, results shared
34
+ - 🗑️ **Auto Cleanup** - Memory management handled automatically
35
+ - 🔧 **Dual Programming Paradigms** - Method chaining or functional pipe composition
36
+ - 🛠️ **Custom Transformers** - Create reusable transformers in both OOP and functional styles
37
+ - 📊 **Reactive Collections** - Lists, Maps, Sets with fine-grained change events
38
+ - 🔀 **Stateful Operations** - Built-in support for stateful filtering, mapping, and grouping
39
+ - 📦 **Zero Dependencies** - Lightweight (~6KB minified, ~2KB gzipped) and tree-shakeable
40
+ - 🌐 **Universal** - Node.js, browsers, Deno, Bun, Cloudflare Workers
41
+ - 📘 **Full TypeScript** - Complete type safety and inference
42
+
43
+ ## Quick Start
29
44
 
30
45
  ```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));
46
+ import { Stream, State } from "@soffinal/stream";
40
47
 
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
- ```
48
+ // Create reactive data streams
49
+ const events = new Stream<string>();
50
+ const counter = new State(0);
49
51
 
50
- ### Async Iteration: Processing Data as it Flows
52
+ // Transform and filter data
53
+ const processed = events.filter((msg) => msg.length > 3).map((msg) => msg.toUpperCase());
51
54
 
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
- ```
55
+ // Multiple consumers
56
+ processed.listen((msg) => console.log("Logger:", msg));
57
+ processed.listen((msg) => updateUI(msg));
67
58
 
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
- });
59
+ // Push data through the pipeline
60
+ events.push("hello", "hi", "world"); // Only 'HELLO' and 'WORLD' processed
81
61
 
82
- sensorStream.listen((data) => console.log("Sensor reading:", data));
62
+ // Reactive state management
63
+ counter.listen((count) => (document.title = `Count: ${count}`));
64
+ counter.value++; // UI updates automatically
83
65
  ```
84
66
 
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
- ```
67
+ ## Installation
114
68
 
115
- #### Map: Data Transformation Pipeline
69
+ ### Package Managers
116
70
 
117
- Mapping transforms data while maintaining the stream's async nature:
71
+ ```bash
72
+ # npm
73
+ npm install @soffinal/stream
118
74
 
119
- ```typescript
120
- const rawEvents = new Stream<string>();
75
+ # yarn
76
+ yarn add @soffinal/stream
121
77
 
122
- // Simple transformation
123
- const parsedEvents = rawEvents.map((json) => JSON.parse(json));
78
+ # pnpm
79
+ pnpm add @soffinal/stream
124
80
 
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
- );
81
+ # bun
82
+ bun add @soffinal/stream
130
83
 
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
- });
84
+ # Deno
85
+ deno add jsr:@soffinal/stream
136
86
  ```
137
87
 
138
- #### Merge: Combine Multiple Streams
88
+ ### CDN (Browser)
139
89
 
140
- Combine multiple streams into a single output stream:
90
+ ```html
91
+ <!-- Production (minified) -->
92
+ <script type="module">
93
+ import { Stream, State } from "https://cdn.jsdelivr.net/npm/@soffinal/stream@latest/dist/index.js";
94
+ </script>
141
95
 
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");
96
+ <!-- Alternative CDNs -->
97
+ <script type="module">
98
+ import { Stream } from "https://esm.sh/@soffinal/stream";
99
+ import { Stream } from "https://cdn.skypack.dev/@soffinal/stream";
100
+ </script>
151
101
  ```
152
102
 
153
- #### Group: Intelligent Batching
154
-
155
- Grouping allows you to batch data based on complex conditions:
103
+ ## Core Concepts
156
104
 
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);
105
+ ### Streams: Async Data Pipelines
162
106
 
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:
107
+ A `Stream` is an async iterable that can push values to multiple listeners while also being awaitable for the next value.
181
108
 
182
109
  ```typescript
183
- const arrays = new Stream<number[]>();
184
- const flattened = arrays.flat();
110
+ const userEvents = new Stream<UserEvent>();
185
111
 
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
112
+ // Multiple consumers
113
+ userEvents.listen((event) => analytics.track(event));
114
+ userEvents.listen((event) => notifications.send(event));
115
+ userEvents.listen((event) => database.save(event));
189
116
 
190
- // Deep flattening
191
- const nested = new Stream<number[][][]>();
192
- const deepFlat = nested.flat(2);
117
+ // Or await the next event
118
+ const nextEvent = await userEvents;
193
119
  ```
194
120
 
195
- #### Promise Interface
121
+ ### Transformers: Fluent & Functional Styles
196
122
 
197
- Streams are directly awaitable for the next event/data:
123
+ #### Method Chaining (Fluent)
198
124
 
199
125
  ```typescript
200
126
  const stream = new Stream<number>();
201
127
 
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>();
128
+ // Simple transformations
129
+ const result = stream
130
+ .filter((x) => x > 0)
131
+ .map((x) => x * 2)
132
+ .group((batch) => batch.length >= 5);
242
133
 
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
134
+ // Async operations with order preservation
135
+ const asyncProcessed = stream
136
+ .filter(async (value) => {
137
+ const isValid = await validateAsync(value);
138
+ return isValid;
139
+ })
140
+ .map(async (value) => {
141
+ const enriched = await enrichWithExternalData(value);
142
+ return { original: value, enriched };
143
+ });
249
144
 
250
- result.listen((value) => console.log("Processed:", value));
145
+ // Stateful operations with initial state
146
+ const stateful = stream
147
+ .filter(0, (prev, curr) => [curr > prev, Math.max(prev, curr)]) // Only increasing values
148
+ .map([], (history, value) => {
149
+ const newHistory = [...history, value].slice(-3); // Keep last 3
150
+ const average = newHistory.reduce((a, b) => a + b, 0) / newHistory.length;
151
+ return [{ value, average, history: newHistory }, newHistory];
152
+ })
153
+ .group(5, (count, item) => [count >= 5, count >= 5 ? 0 : count + 1]); // Batch every 5
251
154
  ```
252
155
 
253
- ### Built-in Transformer Functions
254
-
255
- The library provides irreducible building block transformers that work seamlessly with pipe:
156
+ #### Functional Composition (Pipe)
256
157
 
257
158
  ```typescript
258
- // Import transformer functions
259
- import { map, filter, group, merge, flat } from "@soffinal/stream";
159
+ import { filter, map, group, merge } from "@soffinal/stream";
260
160
 
261
- const stream = new Stream<number>();
161
+ // Simple functional composition
162
+ const result = stream
163
+ .pipe(filter((x) => x > 0))
164
+ .pipe(map((x) => x * 2))
165
+ .pipe(group((batch) => batch.length >= 5));
262
166
 
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>
167
+ // Async functional transformers (order preserved)
168
+ const asyncPipeline = stream
169
+ .pipe(filter(async (x) => await isValidAsync(x)))
170
+ .pipe(map(async (x) => await processAsync(x)))
171
+ .pipe(group(batch => batch.length >= 5));
268
172
 
269
- // Stateful transformations
270
- stream
173
+ // Stateful functional transformers
174
+ const advanced = stream
271
175
  .pipe(filter(0, (prev, curr) => [curr > prev, Math.max(prev, curr)]))
272
- .pipe(map(0, (sum, n) => [sum + n, sum + n]));
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]));
273
184
 
274
- // Advanced transformations
185
+ // Combining multiple streams
275
186
  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
187
+ const merged = stream
188
+ .pipe(filter((x) => x % 2 === 0))
189
+ .pipe(merge(stream2))
190
+ .pipe(map((x) => x.toString()));
280
191
  ```
281
192
 
282
- ### Custom Transformers: Building Your Own
193
+ ### Custom Transformers
283
194
 
284
- You can create custom transformers using generator functions:
195
+ Create reusable transformers for both paradigms:
285
196
 
286
197
  ```typescript
287
- // DistinctUntilChanged transformer - only emit when value changes
198
+ // Custom transformer: distinctUntilChanged
288
199
  const distinctUntilChanged = <T>(stream: Stream<T>): Stream<T> => {
289
200
  return new Stream<T>(async function* () {
290
201
  let prev: T;
@@ -300,7 +211,7 @@ const distinctUntilChanged = <T>(stream: Stream<T>): Stream<T> => {
300
211
  });
301
212
  };
302
213
 
303
- // Rate limiting transformer
214
+ // Custom transformer: throttle
304
215
  const throttle =
305
216
  <T>(ms: number) =>
306
217
  (stream: Stream<T>): Stream<T> => {
@@ -320,272 +231,144 @@ const throttle =
320
231
  // Usage with perfect type inference
321
232
  const searchInput = new Stream<string>();
322
233
  const searchResults = searchInput
323
- .pipe(distinctUntilChanged) // Only emit when search term changes
324
- .pipe(throttle(1000)) // Limit API calls
234
+ .pipe(distinctUntilChanged)
235
+ .pipe(throttle(300))
325
236
  .pipe(map((query) => searchAPI(query)));
326
237
  ```
327
238
 
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)
239
+ ### Reactive State
348
240
 
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:
241
+ State objects hold current values and notify dependents of changes:
366
242
 
367
243
  ```typescript
368
- import { State } from "@soffinal/stream";
369
-
370
- // Application state
371
244
  const user = new State<User | null>(null);
372
245
  const theme = new State<"light" | "dark">("light");
373
- const notifications = new State<Notification[]>([]);
374
246
 
375
- // Reactive computations
247
+ // Derived state with transformations
376
248
  const isLoggedIn = user.map((u) => u !== null);
377
- const unreadCount = notifications.map((n) => n.filter((x) => !x.read).length);
249
+ const userProfile = user
250
+ .filter((u): u is User => u !== null)
251
+ .map((u) => ({ ...u, displayName: u.firstName + " " + u.lastName }));
378
252
 
379
253
  // Automatic UI updates
380
254
  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";
255
+ document.body.classList.toggle("authenticated", loggedIn);
386
256
  });
387
257
  ```
388
258
 
389
- ### State Management Example
259
+ ### Reactive Collections
390
260
 
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:
261
+ Collections that emit fine-grained change events:
438
262
 
439
263
  ```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
- }
264
+ const todos = new List<Todo>();
265
+ const cache = new Map<string, any>();
266
+ const activeUsers = new Set<string>();
464
267
 
465
- // Automatic persistence
466
- todos.insert.listen(() => saveTodosToStorage([...todos]));
467
- todos.delete.listen(() => saveTodosToStorage([...todos]));
268
+ // React to changes
269
+ todos.insert.listen(([index, todo]) => renderTodo(todo, index));
270
+ cache.set.listen(([key, value]) => console.log(`Cached: ${key}`));
271
+ activeUsers.add.listen((userId) => showOnlineStatus(userId));
468
272
  ```
469
273
 
470
- ### Map: Reactive Key-Value Store
274
+ ## API Reference
471
275
 
472
- Key-value store that emits events on changes:
276
+ ### Stream\<T>
473
277
 
474
- ```typescript
475
- import { Map } from "@soffinal/stream";
278
+ #### Properties
476
279
 
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
- });
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
483
283
 
484
- // Listen to cache evictions
485
- cache.delete.listen(([key, value]) => {
486
- console.log(`Cache evicted: ${key}`);
487
- });
284
+ #### Methods
488
285
 
489
- // Listen to clear events
490
- cache.clear.listen(() => console.log("Cache cleared"));
286
+ - `push(...values: T[]): void` - Emit values to all listeners
287
+ - `listen(callback: (value: T) => void, signal?: AbortSignal): () => void` - Add listener, returns cleanup function
288
+ - `filter<U extends T>(predicate: (value: T) => value is U): Stream<U>` - Filter with type guard
289
+ - `filter(predicate: (value: T) => boolean | Promise<boolean>): Stream<T>` - Filter with async predicate
290
+ - `filter<S>(initialState: S, accumulator: (state: S, value: T) => [boolean, S] | Promise<[boolean, S]>): Stream<T>` - Stateful filtering
291
+ - `map<U>(mapper: (value: T) => U | Promise<U>): Stream<U>` - Transform with async mapper
292
+ - `map<S, U>(initialState: S, accumulator: (state: S, value: T) => [U, S] | Promise<[U, S]>): Stream<U>` - Stateful mapping
293
+ - `group(predicate: (batch: T[]) => boolean | Promise<boolean>): Stream<T[]>` - Group into batches
294
+ - `group<S>(initialState: S, accumulator: (state: S, value: T) => [boolean, S] | Promise<[boolean, S]>): Stream<S>` - Stateful grouping
295
+ - `merge<STREAMS extends [Stream<any>, ...Stream<any>[]]>(...streams: STREAMS): Stream<T | ValueOf<STREAMS[number]>>` - Merge multiple streams
296
+ - `flat<DEPTH extends number = 0>(depth?: DEPTH): Stream<FlatArray<T, DEPTH>>` - Flatten arrays with configurable depth
297
+ - `pipe<U>(transformer: (stream: Stream<T>) => Stream<U>): Stream<U>` - Apply functional transformer
491
298
 
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)
299
+ #### Async Interface
496
300
 
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
- ```
301
+ - `then<U>(callback?: (value: T) => U): Promise<U>` - Promise interface
302
+ - `[Symbol.asyncIterator](): AsyncIterator<T>` - Async iteration
504
303
 
505
- ### Set: Reactive Unique Collections
304
+ ### State\<T> extends Stream\<T>
506
305
 
507
- Unique value collection that emits events on changes:
306
+ #### Properties
508
307
 
509
- ```typescript
510
- import { Set } from "@soffinal/stream";
308
+ - `value: T` - Current state value (get/set)
511
309
 
512
- const activeUsers = new Set<string>();
310
+ ### Transformer Functions
513
311
 
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
- });
312
+ Functional transformers for use with `pipe()` - all support async operations:
519
313
 
520
- // Listen to deletions
521
- activeUsers.delete.listen((userId) => {
522
- console.log(`User ${userId} went offline`);
523
- broadcastUserStatus(userId, "offline");
524
- });
314
+ #### filter
315
+ - `filter<T, U extends T>(predicate: (value: T) => value is U): (stream: Stream<T>) => Stream<U>`
316
+ - `filter<T>(predicate: (value: T) => boolean | Promise<boolean>): (stream: Stream<T>) => Stream<T>`
317
+ - `filter<T, S>(initialState: S, accumulator: (state: S, value: T) => [boolean, S] | Promise<[boolean, S]>): (stream: Stream<T>) => Stream<T>`
525
318
 
526
- // Listen to clear events
527
- activeUsers.clear.listen(() => console.log("All users cleared"));
319
+ #### map
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>`
528
322
 
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)
323
+ #### group
324
+ - `group<T>(predicate: (batch: T[]) => boolean | Promise<boolean>): (stream: Stream<T>) => Stream<T[]>`
325
+ - `group<T, S>(initialState: S, accumulator: (state: S, value: T) => [boolean, S] | Promise<[boolean, S]>): (stream: Stream<T>) => Stream<S>`
533
326
 
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
- ```
327
+ #### merge
328
+ - `merge<STREAMS extends [Stream<any>, ...Stream<any>[]]>(...streams: STREAMS): <T>(stream: Stream<T>) => Stream<T | ValueOf<STREAMS[number]>>`
541
329
 
542
- ## Use Cases
330
+ #### flat
331
+ - `flat(): <T>(stream: Stream<T>) => Stream<FlatArray<T, 0>>`
332
+ - `flat<DEPTH extends number>(depth: DEPTH): <T>(stream: Stream<T>) => Stream<FlatArray<T, DEPTH>>`
543
333
 
544
- Practical examples of building reactive applications.
334
+ ### Reactive Collections
545
335
 
546
- ### Real-time Data Processing
336
+ #### List\<T>
547
337
 
548
- Process sensor data with filtering and transformation:
338
+ - `insert: Stream<[number, T]>` - Insertion events
339
+ - `delete: Stream<[number, T]>` - Deletion events
340
+ - `clear: Stream<void>` - Clear events
549
341
 
550
- ```typescript
551
- const sensorData = new Stream<{ temperature: number; humidity: number }>();
342
+ #### Map\<K,V> extends globalThis.Map\<K,V>
552
343
 
553
- // Alert system
554
- sensorData.filter((data) => data.temperature > 30).listen((data) => console.log("High temperature alert:", data));
344
+ - `set: Stream<[K, V]>` - Set events (only on changes)
345
+ - `delete: Stream<[K, V]>` - Delete events
346
+ - `clear: Stream<void>` - Clear events
555
347
 
556
- // Data logging
557
- sensorData.map((data) => ({ ...data, timestamp: Date.now() })).listen((data) => saveToDatabase(data));
348
+ #### Set\<T> extends globalThis.Set\<T>
558
349
 
559
- // Simulate sensor readings
560
- setInterval(() => {
561
- sensorData.push({
562
- temperature: Math.random() * 40,
563
- humidity: Math.random() * 100,
564
- });
565
- }, 1000);
566
- ```
350
+ - `add: Stream<T>` - Add events (only new values)
351
+ - `delete: Stream<T>` - Delete events
352
+ - `clear: Stream<void>` - Clear events
567
353
 
568
- ### Event Aggregation
354
+ ## Examples
569
355
 
570
- Collect and batch user events for analytics:
356
+ ### Real-time Data Processing
571
357
 
572
358
  ```typescript
573
- const userActions = new Stream<{ userId: string; action: string }>();
359
+ const sensorData = new Stream<SensorReading>();
574
360
 
575
- // Group actions by time windows
576
- const actionBatches = userActions.group(0, (count, action) => {
577
- return count >= 10 ? [true, 0] : [false, count + 1];
578
- });
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);
579
366
 
580
- actionBatches.listen((count) => {
581
- console.log(`Processed ${count} actions`);
582
- });
367
+ alerts.listen((batch) => sendAlertNotification(batch));
583
368
  ```
584
369
 
585
370
  ### WebSocket Integration
586
371
 
587
- Create reactive streams from WebSocket connections:
588
-
589
372
  ```typescript
590
373
  const wsStream = new Stream<MessageEvent>(async function* () {
591
374
  const ws = new WebSocket("wss://api.example.com");
@@ -597,214 +380,150 @@ const wsStream = new Stream<MessageEvent>(async function* () {
597
380
  }
598
381
  });
599
382
 
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:
383
+ const messages = wsStream.map((event) => JSON.parse(event.data)).filter((data) => data.type === "update");
609
384
 
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));
385
+ messages.listen((update) => handleUpdate(update));
650
386
  ```
651
387
 
652
- ### Cleanup and Memory Management
653
-
654
- Properly clean up listeners and prevent memory leaks:
388
+ ### State Management
655
389
 
656
390
  ```typescript
657
- const stream = new Stream<number>();
391
+ interface AppState {
392
+ user: User | null;
393
+ theme: "light" | "dark";
394
+ notifications: Notification[];
395
+ }
658
396
 
659
- // Automatic cleanup with AbortSignal
660
- const controller = new AbortController();
661
- stream.listen((value) => console.log(value), controller.signal);
397
+ const appState = new State<AppState>({
398
+ user: null,
399
+ theme: "light",
400
+ notifications: [],
401
+ });
662
402
 
663
- // Clean up when component unmounts
664
- setTimeout(() => controller.abort(), 5000);
403
+ // Derived state
404
+ const unreadCount = appState.map((state) => state.notifications.filter((n) => !n.read).length);
665
405
 
666
- // Or manual cleanup
667
- const cleanup = stream.listen((value) => console.log(value));
668
- setTimeout(cleanup, 5000);
406
+ // Reactive UI
407
+ unreadCount.listen((count) => {
408
+ document.title = count > 0 ? `(${count}) App` : "App";
409
+ });
669
410
  ```
670
411
 
671
- ## Installation
672
-
673
- # bun
674
-
675
- ```bash
676
- bun add @soffinal/stream
677
- # or from JSR
678
- bunx jsr add @soffinal/stream
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>
679
440
  ```
680
441
 
681
- # Deno
442
+ ## Performance
682
443
 
683
- ```bash
684
- deno add npm:@soffinal/stream
685
- # or from JSR
686
- deno add jsr:@soffinal/stream
444
+ @soffinal/stream delivers exceptional performance:
687
445
 
688
- ```
446
+ - **High throughput** for basic operations
447
+ - **Efficient processing** for complex pipelines
448
+ - **Memory efficient** with automatic cleanup
449
+ - **Zero overhead** for unused features
689
450
 
690
- # npm
451
+ ### Performance Characteristics
691
452
 
692
- ```bash
693
- npm install @soffinal/stream
694
- # or from JSR
695
- npx jsr add @soffinal/stream
696
- ```
453
+ - **Optimized data structures** for minimal memory allocation
454
+ - **Efficient event dispatch** with smart caching
455
+ - **Lazy evaluation** reduces unnecessary computations
456
+ - **Automatic cleanup** prevents memory leaks
697
457
 
698
- # pnpm
458
+ ## Browser Support
699
459
 
700
- ```bash
701
- pnpm install @soffinal/stream
702
- # or from JSR
703
- pnpm install jsr:@soffinal/stream
460
+ - **Modern browsers** supporting ES2020+
461
+ - **Node.js** 16+
462
+ - **Deno** 1.0+
463
+ - **Bun** 1.0+
464
+ - **Cloudflare Workers**
704
465
 
705
- ```
466
+ ## Migration Guide
706
467
 
707
- # yarn
468
+ ### From RxJS
708
469
 
709
- ```bash
710
- yarn add @soffinal/stream
711
- yarn add jsr:@soffinal/stream
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);
712
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);
713
488
  ```
714
489
 
715
- ## API Reference
716
-
717
- ### Stream<T>
718
-
719
- **Properties:**
720
-
721
- - `hasListeners: boolean` - True if stream has active listeners
722
- - `listenerAdded: Stream<void>` - Emits when listener is added
723
- - `listenerRemoved: Stream<void>` - Emits when listener is removed
724
-
725
- **Methods:**
726
-
727
- - `push(...values: T[]): void` - Emit values to all listeners
728
- - `listen(callback: (value: T) => void, signal?: AbortSignal): () => void` - Add listener, returns cleanup function
729
- - `filter<U extends T>(predicate: (value: T) => value is U): Stream<U>` - Filter values with type guard
730
- - `filter(predicate: (value: T) => boolean | Promise<boolean>): Stream<T>` - Filter values with predicate
731
- - `filter<S>(initialState: S, accumulator: (state: S, value: T) => [boolean, S] | Promise<[boolean, S]>): Stream<T>` - Stateful filtering
732
- - `map<U>(mapper: (value: T) => U | Promise<U>): Stream<U>` - Transform values
733
- - `map<S, U>(initialState: S, accumulator: (state: S, value: T) => [U, S] | Promise<[U, S]>): Stream<U>` - Stateful mapping
734
- - `merge(...streams: Stream<T>[]): Stream<T>` - Combine multiple streams
735
- - `group(predicate: (batch: T[]) => boolean): Stream<T[]>` - Group values into batches
736
- - `group<S>(initialState: S, accumulator: (state: S, value: T) => [boolean, S]): Stream<T[]>` - Stateful grouping
737
- - `flat<U>(depth?: number): Stream<U>` - Flatten array values (default depth: 0)
738
- - `pipe<U>(transformer: (stream: Stream<T>) => Stream<U>): Stream<U>` - Apply functional transformer
739
- - `then<U>(callback?: (value: T) => U): Promise<U>` - Promise-like interface for first value
740
- - `[Symbol.asyncIterator](): AsyncIterator<T>` - Async iteration support
741
-
742
- ### Transformer Functions
743
-
744
- **Built-in transformers for functional composition:**
490
+ ### From EventEmitter
745
491
 
746
- - `map<T, U>(mapper: (value: T) => U | Promise<U>): (stream: Stream<T>) => Stream<U>` - Transform values
747
- - `map<T, S, U>(initialState: S, accumulator: (state: S, value: T) => [U, S] | Promise<[U, S]>): (stream: Stream<T>) => Stream<U>` - Stateful mapping
748
- - `filter<T, U extends T>(predicate: (value: T) => value is U): (stream: Stream<T>) => Stream<U>` - Type guard filtering
749
- - `filter<T>(predicate: (value: T) => boolean | Promise<boolean>): (stream: Stream<T>) => Stream<T>` - Predicate filtering
750
- - `filter<T, S>(initialState: S, accumulator: (state: S, value: T) => [boolean, S] | Promise<[boolean, S]>): (stream: Stream<T>) => Stream<T>` - Stateful filtering
751
- - `group<T>(predicate: (batch: T[]) => boolean): (stream: Stream<T>) => Stream<T[]>` - Group values into batches
752
- - `group<T, S>(initialState: S, accumulator: (state: S, value: T) => [boolean, S]): (stream: Stream<T>) => Stream<T[]>` - Stateful grouping
753
- - `merge<T>(...streams: Stream<T>[]): (stream: Stream<T>) => Stream<T>` - Merge multiple streams into one
754
- - `flat<T, U>(depth?: number): (stream: Stream<T>) => Stream<U>` - Flatten array values with configurable depth
755
-
756
- ### State<T> extends Stream<T>
757
-
758
- **Properties:**
759
-
760
- - `value: T` - Get/set current state value
761
-
762
- **Methods:**
763
-
764
- - `push(...values: T[]): void` - Update state with multiple values sequentially
765
-
766
- ### List<T>
767
-
768
- **Properties:**
769
-
770
- - `length: number` - Current list length
771
- - `insert: Stream<[number, T]>` - Emits on insertions
772
- - `delete: Stream<[number, T]>` - Emits on deletions
773
- - `clear: Stream<void>` - Emits on clear
774
- - `[index]: T` - Index access with modulo wrapping
775
-
776
- **Methods:**
777
-
778
- - `insert(index: number, value: T): void` - Insert value at index
779
- - `delete(index: number): T | undefined` - Delete value at index, returns deleted value
780
- - `clear(): void` - Clear all items
781
- - `get(index: number): T | undefined` - Get value without modulo wrapping
782
- - `values(): IterableIterator<T>` - Iterator for values
783
-
784
- ### Map<K,V> extends globalThis.Map<K,V>
785
-
786
- **Stream Properties:**
492
+ ```typescript
493
+ // EventEmitter
494
+ import { EventEmitter } from "events";
495
+ const emitter = new EventEmitter();
496
+ emitter.on("data", console.log);
497
+ emitter.emit("data", "hello");
787
498
 
788
- - `set: Stream<[K, V]>` - Emits on set operation (only actual changes)
789
- - `delete: Stream<[K, V]>` - Emits on successful deletions
790
- - `clear: Stream<void>` - Emits on clear (only if not empty)
499
+ // @soffinal/stream
500
+ import { Stream } from "@soffinal/stream";
501
+ const stream = new Stream();
502
+ stream.listen(console.log);
503
+ stream.push("hello");
504
+ ```
791
505
 
792
- **Methods:**
506
+ ## Contributing
793
507
 
794
- - All native Map methods plus reactive stream properties
508
+ We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
795
509
 
796
- ### Set<T> extends globalThis.Set<T>
510
+ ### Development Setup
797
511
 
798
- **Stream Properties:**
512
+ ```bash
513
+ git clone https://github.com/soffinal/stream.git
514
+ cd stream
515
+ bun install
516
+ bun test
517
+ ```
799
518
 
800
- - `add: Stream<T>` - Emits on successful additions (no duplicates)
801
- - `delete: Stream<T>` - Emits on successful deletions
802
- - `clear: Stream<void>` - Emits on clear (only if not empty)
519
+ ## License
803
520
 
804
- **Methods:**
521
+ MIT © [Soffinal](https://github.com/soffinal)
805
522
 
806
- - All native Set methods plus reactive stream properties
523
+ Contact: <smari.sofiane@gmail.com>
807
524
 
808
- MIT License
525
+ ---
809
526
 
810
- Copyright (c) 2024 Soffinal <https://github.com/soffinal> <smari.sofiane@gmail.com>
527
+ <div align="center">
528
+ <strong>Built with ❤️ for the JavaScript community</strong>
529
+ </div>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@soffinal/stream",
3
3
  "module": "./dist/index.js",
4
- "version": "0.1.2",
4
+ "version": "0.1.4",
5
5
  "description": "A reactive event streaming library for TypeScript/JavaScript",
6
6
  "type": "module",
7
7
  "devDependencies": {