@soffinal/stream 0.2.1 → 0.2.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.
@@ -1,216 +1,430 @@
1
1
  # Map Transformer
2
2
 
3
- ## The Adaptive Alchemist
3
+ The `map` transformer transforms values flowing through a stream. It supports synchronous and asynchronous transformations, stateful operations, type conversions, and multiple concurrency strategies for optimal performance.
4
4
 
5
- Traditional mapping is a simple function application - transform input A to output B. But real-world transformation often requires **context, memory, and evolution**. The `map` transformer embodies **Adaptive Reactive Programming** - where the alchemist remembers previous transformations and evolves its craft.
5
+ ## Quick Start
6
6
 
7
- ## Design
7
+ ```typescript
8
+ import { Stream, map } from "@soffinal/stream";
9
+
10
+ const numbers = new Stream<number>();
11
+
12
+ // Simple transformation
13
+ const doubled = numbers.pipe(map((n) => n * 2));
14
+
15
+ doubled.listen(console.log);
16
+ numbers.push(1, 2, 3); // Outputs: 2, 4, 6
17
+ ```
8
18
 
9
- ### Why State-First Architecture?
19
+ ## Basic Usage
20
+
21
+ ### Synchronous Transformations
10
22
 
11
23
  ```typescript
12
- map(initialState, (state, value) => [transformed, newState]);
24
+ // Number to string
25
+ stream.pipe(map((n) => n.toString()));
26
+
27
+ // Object property extraction
28
+ stream.pipe(map((user) => user.name));
29
+
30
+ // Complex transformations
31
+ stream.pipe(
32
+ map((data) => ({
33
+ id: data.id,
34
+ name: data.name.toUpperCase(),
35
+ timestamp: Date.now(),
36
+ }))
37
+ );
13
38
  ```
14
39
 
15
- **State comes first** because transformation is contextual. We don't transform values in isolation - we transform them based on what we've learned, what we've seen, and where we're going. The state is the accumulated knowledge; the value is just the raw material.
40
+ ### Type Transformations
16
41
 
17
- ### The Dual Return Pattern
42
+ Map excels at converting between types with full TypeScript inference:
18
43
 
19
44
  ```typescript
20
- return [transformedValue, newState]; // Transform and evolve
45
+ const numbers = new Stream<number>();
46
+
47
+ // number → string
48
+ const strings = numbers.pipe(map((n) => n.toString()));
49
+
50
+ // number → object
51
+ const objects = numbers.pipe(
52
+ map((n) => ({
53
+ value: n,
54
+ squared: n * n,
55
+ isEven: n % 2 === 0,
56
+ }))
57
+ );
58
+
59
+ // Chained transformations
60
+ const result = numbers
61
+ .pipe(map((n) => n * 2)) // number → number
62
+ .pipe(map((n) => n.toString())) // number → string
63
+ .pipe(map((s) => s.length)); // string → number
21
64
  ```
22
65
 
23
- **Every transformation teaches us something.** The dual return forces us to consider: "How does this transformation change our understanding?" Even if the state doesn't change, we must consciously decide that.
66
+ ## Asynchronous Transformations
24
67
 
25
- ### Argument Order
68
+ ### Sequential Processing (Default)
26
69
 
27
70
  ```typescript
28
- (state, value) => // Context first, content second
71
+ const urls = new Stream<string>();
72
+
73
+ const responses = urls.pipe(
74
+ map(async (url) => {
75
+ const response = await fetch(url);
76
+ return await response.json();
77
+ })
78
+ );
79
+
80
+ // Requests processed one at a time, maintaining order
29
81
  ```
30
82
 
31
- **Context shapes transformation.** A value of `5` might become `10` (double), `"5"` (stringify), or `{ count: 5, timestamp: now }` (enrich) - depending on the accumulated state. The transformer's history determines the value's destiny.
83
+ ### Concurrent Strategies
84
+
85
+ For expensive async operations, choose a concurrency strategy:
32
86
 
33
- ## The Adaptive Transformation System
87
+ #### Concurrent Unordered
34
88
 
35
- ### Level 1: Simple Alchemy
89
+ Transformations run in parallel, results emit as they complete:
36
90
 
37
91
  ```typescript
38
- // Traditional transformation - no memory, no learning
39
- stream.pipe(map({}, (_, value) => [value * 2, {}]));
92
+ const ids = new Stream<string>();
93
+
94
+ const users = ids.pipe(
95
+ map(
96
+ async (id) => {
97
+ const user = await fetchUser(id);
98
+ return user;
99
+ },
100
+ { strategy: "concurrent-unordered" }
101
+ )
102
+ );
103
+
104
+ // Users emit as API calls complete, potentially out of order
40
105
  ```
41
106
 
42
- Even "simple" mapping uses the adaptive architecture. The empty state `{}` represents a state that doesn't need memory - but could develop it.
107
+ #### Concurrent Ordered
43
108
 
44
- ### Level 2: Contextual Transformation
109
+ Parallel processing but maintains original order:
45
110
 
46
111
  ```typescript
47
- // The alchemist remembers and enriches
48
- stream.pipe(
49
- map({ sum: 0, count: 0 }, (state, value) => {
50
- const newSum = state.sum + value;
51
- const newCount = state.count + 1;
52
- const average = newSum / newCount;
112
+ const images = new Stream<string>();
113
+
114
+ const processed = images.pipe(
115
+ map(
116
+ async (imageUrl) => {
117
+ const processed = await processImage(imageUrl);
118
+ return processed;
119
+ },
120
+ { strategy: "concurrent-ordered" }
121
+ )
122
+ );
53
123
 
54
- return [
55
- { value, runningSum: newSum, runningAverage: average },
56
- { sum: newSum, count: newCount },
57
- ];
124
+ // Images always emit in original order despite varying processing times
125
+ ```
126
+
127
+ ## Stateful Transformations
128
+
129
+ Maintain state across transformations for complex operations:
130
+
131
+ ### Basic Stateful Mapping
132
+
133
+ ```typescript
134
+ const numbers = new Stream<number>();
135
+
136
+ // Running sum
137
+ const runningSums = numbers.pipe(
138
+ map({ sum: 0 }, (state, value) => {
139
+ const newSum = state.sum + value;
140
+ return [newSum, { sum: newSum }];
58
141
  })
59
142
  );
143
+
144
+ numbers.push(1, 2, 3, 4);
145
+ // Outputs: 1, 3, 6, 10
60
146
  ```
61
147
 
62
- ### Level 3: Evolutionary Transformation
148
+ ### Indexing and Counting
63
149
 
64
150
  ```typescript
65
- // The alchemist adapts its formula based on patterns
66
- stream.pipe(
67
- map({ multiplier: 1, trend: "stable" }, (state, value) => {
68
- // Adapt the transformation based on observed patterns
69
- const newMultiplier = value > 100 ? state.multiplier * 1.1 : state.multiplier * 0.9;
70
- const trend = newMultiplier > state.multiplier ? "growing" : "shrinking";
151
+ const items = new Stream<string>();
71
152
 
72
- return [value * newMultiplier, { multiplier: newMultiplier, trend }];
153
+ // Add indices
154
+ const indexed = items.pipe(
155
+ map({ index: 0 }, (state, value) => {
156
+ const result = { item: value, index: state.index };
157
+ return [result, { index: state.index + 1 }];
73
158
  })
74
159
  );
75
- ```
76
160
 
77
- **Adaptive transformation** - the formula itself evolves based on the data it processes.
161
+ items.push("a", "b", "c");
162
+ // Outputs: {item: "a", index: 0}, {item: "b", index: 1}, {item: "c", index: 2}
163
+ ```
78
164
 
79
- ### Level 4: Async with Order Preservation
165
+ ### Complex State Management
80
166
 
81
167
  ```typescript
82
- // The alchemist consults external sources while maintaining order
83
- stream.pipe(
84
- map({ cache: new Map() }, async (state, value) => {
85
- if (state.cache.has(value)) {
86
- return [state.cache.get(value), state];
168
+ const events = new Stream<{ type: string; data: any }>();
169
+
170
+ // Event aggregation with history
171
+ const aggregated = events.pipe(
172
+ map(
173
+ {
174
+ counts: new Map<string, number>(),
175
+ history: [] as string[],
176
+ total: 0,
177
+ },
178
+ (state, event) => {
179
+ const newCounts = new Map(state.counts);
180
+ const currentCount = newCounts.get(event.type) || 0;
181
+ newCounts.set(event.type, currentCount + 1);
182
+
183
+ const newHistory = [...state.history, event.type].slice(-10); // Keep last 10
184
+ const newTotal = state.total + 1;
185
+
186
+ const result = {
187
+ event: event.type,
188
+ count: currentCount + 1,
189
+ totalEvents: newTotal,
190
+ recentHistory: newHistory,
191
+ };
192
+
193
+ return [
194
+ result,
195
+ {
196
+ counts: newCounts,
197
+ history: newHistory,
198
+ total: newTotal,
199
+ },
200
+ ];
87
201
  }
202
+ )
203
+ );
204
+ ```
88
205
 
89
- const enriched = await enrichWithAPI(value);
90
- state.cache.set(value, enriched); // Learn for next time
91
- return [enriched, state];
92
- })
206
+ ### Async Stateful Transformations
207
+
208
+ ```typescript
209
+ const requests = new Stream<string>();
210
+
211
+ // Caching with async operations
212
+ const cached = requests.pipe(
213
+ map(
214
+ {
215
+ cache: new Map<string, any>(),
216
+ },
217
+ async (state, url) => {
218
+ // Check cache first
219
+ if (state.cache.has(url)) {
220
+ return [state.cache.get(url), state];
221
+ }
222
+
223
+ // Fetch and cache
224
+ const data = await fetch(url).then((r) => r.json());
225
+ const newCache = new Map(state.cache);
226
+ newCache.set(url, data);
227
+
228
+ return [data, { cache: newCache }];
229
+ }
230
+ )
93
231
  );
94
232
  ```
95
233
 
96
- **Async transformation with memory** - the alchemist doesn't just transform, it **builds institutional knowledge** while preserving the natural order of events.
234
+ ## Performance Considerations
235
+
236
+ ### When to Use Concurrency
97
237
 
98
- ## Essential Copy-Paste Transformers
238
+ - **Sequential**: Default choice, maintains order, lowest overhead
239
+ - **Concurrent-unordered**: Use when order doesn't matter and transformations are expensive
240
+ - **Concurrent-ordered**: Use when order matters but transformations are expensive
99
241
 
100
- ### simpleMap - Gateway to Adaptation
242
+ ### Benchmarking Example
101
243
 
102
244
  ```typescript
103
- // For users transitioning from traditional mapping
104
- const simpleMap = <T, U>(fn: (value: T) => U | Promise<U>) =>
105
- map<T, {}, U>({}, async (_, value) => {
106
- const result = await fn(value);
107
- return [result, {}];
108
- });
245
+ const urls = Array.from({ length: 100 }, (_, i) => `https://api.example.com/item/${i}`);
246
+ const stream = new Stream<string>();
247
+
248
+ // Sequential: ~10 seconds (100ms per request)
249
+ const sequential = stream.pipe(map(async (url) => await fetch(url)));
109
250
 
110
- // Usage: familiar syntax, adaptive foundation
111
- stream.pipe(simpleMap((x) => x * 2));
112
- stream.pipe(simpleMap(async (user) => await enrichUser(user)));
251
+ // Concurrent-unordered: ~1 second (parallel requests)
252
+ const concurrent = stream.pipe(
253
+ map(async (url) => await fetch(url), {
254
+ strategy: "concurrent-unordered",
255
+ })
256
+ );
113
257
  ```
114
258
 
115
- **Design choice**: `simpleMap` is a **bridge**, not a replacement. It introduces users to the adaptive architecture while providing familiar syntax. The empty state `{}` is an invitation to evolution.
259
+ ### Memory Management
116
260
 
117
- ### withIndex - The Counting Alchemist
261
+ For stateful transformations, manage memory carefully:
118
262
 
119
263
  ```typescript
120
- const withIndex = <T>() =>
121
- map<T, { index: number }, { value: T; index: number }>({ index: 0 }, (state, value) => [
122
- { value, index: state.index },
123
- { index: state.index + 1 },
124
- ]);
264
+ // Good: Bounded state
265
+ map(
266
+ {
267
+ recentItems: [] as T[],
268
+ maxSize: 100,
269
+ },
270
+ (state, value) => {
271
+ const newItems = [...state.recentItems, value].slice(-state.maxSize);
272
+ return [
273
+ processItems(newItems),
274
+ {
275
+ recentItems: newItems,
276
+ maxSize: state.maxSize,
277
+ },
278
+ ];
279
+ }
280
+ );
281
+
282
+ // Avoid: Unbounded growth
283
+ map({ history: [] }, (state, value) => {
284
+ // This grows indefinitely!
285
+ return [value, { history: [...state.history, value] }];
286
+ });
125
287
  ```
126
288
 
127
- `withIndex` demonstrates **sequential awareness** - the transformer knows its place in the stream's history and shares that knowledge.
289
+ ## Error Handling
128
290
 
129
- ### delay - The Patient Alchemist
291
+ Handle transformation errors gracefully:
130
292
 
131
293
  ```typescript
132
- const delay = <T>(ms: number) =>
133
- map<T, {}, T>({}, async (_, value) => {
134
- await new Promise((resolve) => setTimeout(resolve, ms));
135
- return [value, {}];
136
- });
294
+ const stream = new Stream<string>();
295
+
296
+ const safe = stream.pipe(
297
+ map(async (data) => {
298
+ try {
299
+ return await riskyTransformation(data);
300
+ } catch (error) {
301
+ console.error("Transformation failed:", error);
302
+ return { error: error.message, original: data };
303
+ }
304
+ })
305
+ );
137
306
  ```
138
307
 
139
- `delay` embodies **temporal transformation** - sometimes the most important transformation is time itself.
308
+ ## Common Patterns
140
309
 
141
- ### pluck - The Focused Alchemist
310
+ ### Enrichment
142
311
 
143
312
  ```typescript
144
- const pluck = <T, K extends keyof T>(key: K) => map<T, {}, T[K]>({}, (_, value) => [value[key], {}]);
145
- ```
313
+ const enrich = <T>(enrichFn: (item: T) => Promise<any>) =>
314
+ map(async (item: T) => {
315
+ const enrichment = await enrichFn(item);
316
+ return { ...item, ...enrichment };
317
+ });
146
318
 
147
- `pluck` demonstrates **selective transformation** - the alchemist knows exactly what it wants and ignores everything else.
319
+ users.pipe(
320
+ enrich(async (user) => ({
321
+ avatar: await getAvatar(user.id),
322
+ permissions: await getPermissions(user.role),
323
+ }))
324
+ );
325
+ ```
148
326
 
149
- ### tap - The Observer Gatekeeper
327
+ ### Batching
150
328
 
151
329
  ```typescript
152
- const tap = <T>(fn: (value: T) => void | Promise<void>) =>
153
- map<T, {}, T>({}, async (_, value) => {
154
- await fn(value);
155
- return [value, {}]; // Always pass through
156
- });
330
+ const batch = <T>(size: number) =>
331
+ map<T, { buffer: T[] }, T[]>({ buffer: [] }, (state, value) => {
332
+ const newBuffer = [...state.buffer, value];
333
+
334
+ if (newBuffer.length >= size) {
335
+ return [newBuffer, { buffer: [] }];
336
+ }
337
+
338
+ return [null, { buffer: newBuffer }];
339
+ }).pipe(filter((batch) => batch !== null));
157
340
 
158
- // Usage: Side effects without changing the stream
159
- stream.pipe(tap((value) => console.log("Saw:", value)));
160
- stream.pipe(tap(async (value) => await logToDatabase(value)));
341
+ stream.pipe(batch(5)); // Emit arrays of 5 items
342
+ ```
343
+
344
+ ### Debouncing with State
345
+
346
+ ```typescript
347
+ const debounce = <T>(ms: number) =>
348
+ map<T, { lastValue: T | null; timer: any }, T | null>(
349
+ {
350
+ lastValue: null,
351
+ timer: null,
352
+ },
353
+ (state, value) => {
354
+ if (state.timer) clearTimeout(state.timer);
355
+
356
+ const timer = setTimeout(() => {
357
+ // This would need additional mechanism to emit
358
+ }, ms);
359
+
360
+ return [null, { lastValue: value, timer }];
361
+ }
362
+ );
161
363
  ```
162
364
 
163
- ### scan - The Accumulating Alchemist
365
+ ### Windowing
164
366
 
165
367
  ```typescript
166
- const scan = <T, U>(fn: (acc: U, value: T) => U, initial: U) =>
167
- map<T, { acc: U }, U>({ acc: initial }, (state, value) => {
168
- const newAcc = fn(state.acc, value);
169
- return [newAcc, { acc: newAcc }];
368
+ const slidingWindow = <T>(size: number) =>
369
+ map<T, { window: T[] }, T[]>({ window: [] }, (state, value) => {
370
+ const newWindow = [...state.window, value].slice(-size);
371
+ return [newWindow, { window: newWindow }];
170
372
  });
171
373
 
172
- // Usage: Accumulate values over time
173
- stream.pipe(scan((sum, value) => sum + value, 0)); // Running sum
174
- stream.pipe(scan((max, value) => Math.max(max, value), -Infinity)); // Running max
374
+ stream.pipe(slidingWindow(3)); // Always emit last 3 items
175
375
  ```
176
376
 
177
- `scan` demonstrates **accumulative transformation** - each value builds upon all previous values, creating a growing understanding.
377
+ ## Advanced Patterns
178
378
 
179
- ## The Order Preservation
180
-
181
- Async transformations maintain order because **sequence matters**. Even if transformation B completes before transformation A, the stream waits. This isn't just about correctness - it's about **respecting the narrative** of the data.
379
+ ### Conditional Transformation
182
380
 
183
381
  ```typescript
184
- // Order is preserved even with varying async delays
382
+ const conditionalMap = <T, U>(condition: (value: T) => boolean, transform: (value: T) => U) =>
383
+ map((value: T) => (condition(value) ? transform(value) : value));
384
+
185
385
  stream.pipe(
186
- map({}, async (_, value) => {
187
- const delay = Math.random() * 1000; // Random processing time
188
- await new Promise((resolve) => setTimeout(resolve, delay));
189
- return [await processValue(value), {}];
190
- })
386
+ conditionalMap(
387
+ (n) => n > 10,
388
+ (n) => n * 2
389
+ )
191
390
  );
192
391
  ```
193
392
 
194
- **Philosophy**: The stream is a story, and stories must be told in order.
393
+ ### Multi-step Processing
195
394
 
196
- ## The State Evolution Pattern
395
+ ```typescript
396
+ const pipeline = <T>(steps: Array<(value: any) => any>) =>
397
+ map((value: T) => steps.reduce((acc, step) => step(acc), value));
197
398
 
198
- State evolution follows a natural progression:
399
+ stream.pipe(
400
+ pipeline([(x) => x.toString(), (x) => x.toUpperCase(), (x) => x.split(""), (x) => x.reverse(), (x) => x.join("")])
401
+ );
402
+ ```
199
403
 
200
- 1. **Empty State** `{}` - The transformer starts innocent
201
- 2. **Simple State** `{ count: 0 }` - It learns to count
202
- 3. **Rich State** `{ sum: 0, count: 0, average: 0 }` - It develops complex understanding
203
- 4. **Intelligent State** `{ cache: Map, patterns: [], predictions: {} }` - It becomes wise
404
+ ## Type Signatures
204
405
 
205
- This mirrors how expertise develops in any field - from simple rules to nuanced understanding.
406
+ ```typescript
407
+ // Simple mapper with optional concurrency
408
+ map<VALUE, MAPPED>(
409
+ mapper: (value: VALUE) => MAPPED | Promise<MAPPED>,
410
+ options?: { strategy: "sequential" | "concurrent-unordered" | "concurrent-ordered" }
411
+ ): (stream: Stream<VALUE>) => Stream<MAPPED>
412
+
413
+ // Stateful mapper (always sequential)
414
+ map<VALUE, STATE, MAPPED>(
415
+ initialState: STATE,
416
+ mapper: (state: STATE, value: VALUE) => [MAPPED, STATE] | Promise<[MAPPED, STATE]>
417
+ ): (stream: Stream<VALUE>) => Stream<MAPPED>
418
+ ```
206
419
 
207
- ## Conclusion
420
+ ## Best Practices
208
421
 
209
- The `map` transformer isn't just about changing values - it's about **intelligent transformation** that:
422
+ 1. **Choose the right strategy**: Use sequential for simple transformations, concurrent for expensive async operations
423
+ 2. **Manage state size**: Keep stateful transformation state bounded to prevent memory leaks
424
+ 3. **Handle errors gracefully**: Wrap risky transformations in try-catch blocks
425
+ 4. **Leverage TypeScript**: Use proper typing for better development experience and runtime safety
426
+ 5. **Consider performance**: Profile your transformations to choose optimal concurrency strategies
427
+ 6. **Compose transformations**: Chain multiple simple maps rather than one complex transformation
428
+ 7. **Use immutable updates**: Always return new state objects in stateful transformations
210
429
 
211
- - **Remembers** previous transformations (state)
212
- - **Learns** from patterns (adaptation)
213
- - **Evolves** its approach over time (constraints)
214
- - **Preserves** the narrative order (respect)
215
- - **Integrates** with reactive state (toState)
216
- - **Accumulates** knowledge over time (scan)
430
+ The map transformer is the workhorse of stream processing, enabling powerful data transformations that scale from simple synchronous operations to complex stateful async processing with optimal performance characteristics.
package/CHANGELOG.md DELETED
@@ -1,22 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [Unreleased]
9
-
10
- ## [0.2.0] - 2024-12-19
11
-
12
- ### Changed
13
-
14
- - **BREAKING**: Removed instance methods for transformers from Stream class - use `.pipe()` exclusively
15
- - **BREAKING**: filte and map transformers signatures now require state-based accumulators instead of simple sync/async predicates
16
- - Improved internal transformer implementation
17
-
18
- ## [0.1.4] - 2024-12-18
19
-
20
- [Unreleased]: https://github.com/soffinal/stream/compare/v0.1.4...HEAD
21
- [0.2.0]: https://github.com/soffinal/stream/compare/v0.1.4...v0.2.0
22
- [0.1.4]: https://github.com/soffinal/stream/releases/tag/v0.1.4