@okikio/observables 1.0.2
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 +578 -0
- package/esm/_dnt.polyfills.d.ts +20 -0
- package/esm/_dnt.polyfills.d.ts.map +1 -0
- package/esm/_dnt.polyfills.js +12 -0
- package/esm/_spec.d.ts +260 -0
- package/esm/_spec.d.ts.map +1 -0
- package/esm/_spec.js +1 -0
- package/esm/_types.d.ts +141 -0
- package/esm/_types.d.ts.map +1 -0
- package/esm/_types.js +20 -0
- package/esm/error.d.ts +331 -0
- package/esm/error.d.ts.map +1 -0
- package/esm/error.js +408 -0
- package/esm/events.d.ts +320 -0
- package/esm/events.d.ts.map +1 -0
- package/esm/events.js +451 -0
- package/esm/helpers/_types.d.ts +188 -0
- package/esm/helpers/_types.d.ts.map +1 -0
- package/esm/helpers/_types.js +1 -0
- package/esm/helpers/mod.d.ts +90 -0
- package/esm/helpers/mod.d.ts.map +1 -0
- package/esm/helpers/mod.js +90 -0
- package/esm/helpers/operations/batch.d.ts +109 -0
- package/esm/helpers/operations/batch.d.ts.map +1 -0
- package/esm/helpers/operations/batch.js +140 -0
- package/esm/helpers/operations/combination.d.ts +162 -0
- package/esm/helpers/operations/combination.d.ts.map +1 -0
- package/esm/helpers/operations/combination.js +350 -0
- package/esm/helpers/operations/conditional.d.ts +211 -0
- package/esm/helpers/operations/conditional.d.ts.map +1 -0
- package/esm/helpers/operations/conditional.js +280 -0
- package/esm/helpers/operations/core.d.ts +198 -0
- package/esm/helpers/operations/core.d.ts.map +1 -0
- package/esm/helpers/operations/core.js +264 -0
- package/esm/helpers/operations/errors.d.ts +277 -0
- package/esm/helpers/operations/errors.d.ts.map +1 -0
- package/esm/helpers/operations/errors.js +378 -0
- package/esm/helpers/operations/mod.d.ts +26 -0
- package/esm/helpers/operations/mod.d.ts.map +1 -0
- package/esm/helpers/operations/mod.js +25 -0
- package/esm/helpers/operations/timing.d.ts +206 -0
- package/esm/helpers/operations/timing.d.ts.map +1 -0
- package/esm/helpers/operations/timing.js +457 -0
- package/esm/helpers/operators.d.ts +520 -0
- package/esm/helpers/operators.d.ts.map +1 -0
- package/esm/helpers/operators.js +563 -0
- package/esm/helpers/pipe.d.ts +118 -0
- package/esm/helpers/pipe.d.ts.map +1 -0
- package/esm/helpers/pipe.js +129 -0
- package/esm/helpers/utils.d.ts +142 -0
- package/esm/helpers/utils.d.ts.map +1 -0
- package/esm/helpers/utils.js +193 -0
- package/esm/mod.d.ts +863 -0
- package/esm/mod.d.ts.map +1 -0
- package/esm/mod.js +861 -0
- package/esm/observable.d.ts +1610 -0
- package/esm/observable.d.ts.map +1 -0
- package/esm/observable.js +1970 -0
- package/esm/package.json +3 -0
- package/esm/queue.d.ts +201 -0
- package/esm/queue.d.ts.map +1 -0
- package/esm/queue.js +273 -0
- package/esm/symbol.d.ts +60 -0
- package/esm/symbol.d.ts.map +1 -0
- package/esm/symbol.js +132 -0
- package/package.json +96 -0
- package/script/_dnt.polyfills.d.ts +20 -0
- package/script/_dnt.polyfills.d.ts.map +1 -0
- package/script/_dnt.polyfills.js +13 -0
- package/script/_spec.d.ts +260 -0
- package/script/_spec.d.ts.map +1 -0
- package/script/_spec.js +2 -0
- package/script/_types.d.ts +141 -0
- package/script/_types.d.ts.map +1 -0
- package/script/_types.js +22 -0
- package/script/error.d.ts +331 -0
- package/script/error.d.ts.map +1 -0
- package/script/error.js +414 -0
- package/script/events.d.ts +320 -0
- package/script/events.d.ts.map +1 -0
- package/script/events.js +458 -0
- package/script/helpers/_types.d.ts +188 -0
- package/script/helpers/_types.d.ts.map +1 -0
- package/script/helpers/_types.js +2 -0
- package/script/helpers/mod.d.ts +90 -0
- package/script/helpers/mod.d.ts.map +1 -0
- package/script/helpers/mod.js +106 -0
- package/script/helpers/operations/batch.d.ts +109 -0
- package/script/helpers/operations/batch.d.ts.map +1 -0
- package/script/helpers/operations/batch.js +144 -0
- package/script/helpers/operations/combination.d.ts +162 -0
- package/script/helpers/operations/combination.d.ts.map +1 -0
- package/script/helpers/operations/combination.js +355 -0
- package/script/helpers/operations/conditional.d.ts +211 -0
- package/script/helpers/operations/conditional.d.ts.map +1 -0
- package/script/helpers/operations/conditional.js +286 -0
- package/script/helpers/operations/core.d.ts +198 -0
- package/script/helpers/operations/core.d.ts.map +1 -0
- package/script/helpers/operations/core.js +272 -0
- package/script/helpers/operations/errors.d.ts +277 -0
- package/script/helpers/operations/errors.d.ts.map +1 -0
- package/script/helpers/operations/errors.js +387 -0
- package/script/helpers/operations/mod.d.ts +26 -0
- package/script/helpers/operations/mod.d.ts.map +1 -0
- package/script/helpers/operations/mod.js +41 -0
- package/script/helpers/operations/timing.d.ts +206 -0
- package/script/helpers/operations/timing.d.ts.map +1 -0
- package/script/helpers/operations/timing.js +464 -0
- package/script/helpers/operators.d.ts +520 -0
- package/script/helpers/operators.d.ts.map +1 -0
- package/script/helpers/operators.js +570 -0
- package/script/helpers/pipe.d.ts +118 -0
- package/script/helpers/pipe.d.ts.map +1 -0
- package/script/helpers/pipe.js +132 -0
- package/script/helpers/utils.d.ts +142 -0
- package/script/helpers/utils.d.ts.map +1 -0
- package/script/helpers/utils.js +200 -0
- package/script/mod.d.ts +863 -0
- package/script/mod.d.ts.map +1 -0
- package/script/mod.js +877 -0
- package/script/observable.d.ts +1610 -0
- package/script/observable.d.ts.map +1 -0
- package/script/observable.js +1984 -0
- package/script/package.json +3 -0
- package/script/queue.d.ts +201 -0
- package/script/queue.d.ts.map +1 -0
- package/script/queue.js +286 -0
- package/script/symbol.d.ts +60 -0
- package/script/symbol.d.ts.map +1 -0
- package/script/symbol.js +135 -0
package/esm/mod.js
ADDED
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A **spec-faithful** yet ergonomic TC39-inspired Observable implementation with
|
|
3
|
+
* detailed TSDocs and examples.
|
|
4
|
+
*
|
|
5
|
+
* Observables are a **push‑based stream abstraction** for events, data, and long‑running
|
|
6
|
+
* operations.
|
|
7
|
+
*
|
|
8
|
+
* If you've ever built a web app, you know the pain: user clicks, API responses, WebSocket messages,
|
|
9
|
+
* timers, file uploads, they all arrive at different times and need different handling. Before Observables,
|
|
10
|
+
* you'd end up with a mess of callbacks, Promise chains, event listeners, and async/await scattered
|
|
11
|
+
* throughout your code.
|
|
12
|
+
*
|
|
13
|
+
* **Observables solve this by giving you one consistent way to handle all async data.**
|
|
14
|
+
*
|
|
15
|
+
* Think of it as a **multi‑value Promise** that keeps sending values until you tell it to stop.
|
|
16
|
+
* Where a Promise gives you one value eventually, an Observable can give you many values over time,
|
|
17
|
+
* mouse clicks, search results, chat messages, sensor readings. And just like Promises have
|
|
18
|
+
* `.then()` and `.catch()`, Observables have operators like `map()`, `filter()`, and `debounce()`
|
|
19
|
+
* to transform data as it flows.
|
|
20
|
+
*
|
|
21
|
+
* ## Why This Exists
|
|
22
|
+
* Apps juggle many async sources, mouse clicks, HTTP requests, timers,
|
|
23
|
+
* WebSockets, file watchers. Before Observables you glued those together with a
|
|
24
|
+
* mish‑mash of callbacks, Promises, `EventTarget`s and async iterators, each
|
|
25
|
+
* with different rules for cleanup and error handling. **Observables give you
|
|
26
|
+
* one mental model** for subscription → cancellation → propagation → teardown.
|
|
27
|
+
*
|
|
28
|
+
* Let's say you're building a search box. Without Observables, you might write something like this:
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* // The messy way: callbacks, timers, and manual cleanup 😫
|
|
32
|
+
* let searchTimeout: number;
|
|
33
|
+
* let lastRequest: Promise<any> | null = null;
|
|
34
|
+
*
|
|
35
|
+
* searchInput.addEventListener('input', async (event) => {
|
|
36
|
+
* const query = event.target.value;
|
|
37
|
+
*
|
|
38
|
+
* // Debounce: wait 300ms after user stops typing
|
|
39
|
+
* clearTimeout(searchTimeout);
|
|
40
|
+
* searchTimeout = setTimeout(async () => {
|
|
41
|
+
*
|
|
42
|
+
* // Cancel previous request somehow?
|
|
43
|
+
* if (lastRequest) {
|
|
44
|
+
* // How do you cancel a fetch? 🤔
|
|
45
|
+
* }
|
|
46
|
+
*
|
|
47
|
+
* if (query.length < 3) return; // Skip short queries
|
|
48
|
+
*
|
|
49
|
+
* try {
|
|
50
|
+
* lastRequest = fetch(`/search?q=${query}`);
|
|
51
|
+
* const response = await lastRequest;
|
|
52
|
+
* const results = await response.json();
|
|
53
|
+
*
|
|
54
|
+
* // Update UI, but what if user already typed something new?
|
|
55
|
+
* updateSearchResults(results);
|
|
56
|
+
* } catch (error) {
|
|
57
|
+
* // Handle errors, but which errors? Network? Parsing?
|
|
58
|
+
* handleSearchError(error);
|
|
59
|
+
* }
|
|
60
|
+
* }, 300);
|
|
61
|
+
* });
|
|
62
|
+
*
|
|
63
|
+
* // Don't forget cleanup when component unmounts!
|
|
64
|
+
* // (Spoiler: everyone forgets this and creates memory leaks)
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* This works, but it's fragile, hard to test, and easy to mess up. Plus, you have to remember to
|
|
68
|
+
* clean up event listeners, cancel timers, and handle edge cases manually.
|
|
69
|
+
*
|
|
70
|
+
* ## The Solution: Observable Pipelines
|
|
71
|
+
*
|
|
72
|
+
* Here's the same search box with Observables:
|
|
73
|
+
*
|
|
74
|
+
* ```ts
|
|
75
|
+
* // The Observable way: clean, composable, and robust ✨
|
|
76
|
+
* import { pipe, debounce, filter, switchMap, map } from './mod.ts';
|
|
77
|
+
*
|
|
78
|
+
* const searchResults = pipe(
|
|
79
|
+
* inputEvents, // Stream of input events
|
|
80
|
+
* debounce(300), // Wait 300ms after user stops typing
|
|
81
|
+
* filter(query => query.length >= 3), // Skip short queries
|
|
82
|
+
* switchMap(query => // Cancel previous requests automatically
|
|
83
|
+
* Observable.from(fetch(`/search?q=${query}`))
|
|
84
|
+
* ),
|
|
85
|
+
* map(response => response.json()) // Parse response
|
|
86
|
+
* );
|
|
87
|
+
*
|
|
88
|
+
* // Subscribe to results (with automatic cleanup!)
|
|
89
|
+
* using subscription = searchResults.subscribe({
|
|
90
|
+
* next: results => updateSearchResults(results),
|
|
91
|
+
* error: error => handleSearchError(error)
|
|
92
|
+
* });
|
|
93
|
+
* // Subscription automatically cleaned up when leaving scope
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* Notice how much cleaner this is? No manual timers, no cancellation logic, no memory leaks.
|
|
97
|
+
* **The operators handle all the complex async stuff for you.**
|
|
98
|
+
*
|
|
99
|
+
* Observables aren't just "nice to have", they solve real problems that bite every developer:
|
|
100
|
+
*
|
|
101
|
+
* - **🧹 Memory Leaks**: Forgot to remove an event listener? Observable subscriptions can clean themselves up.
|
|
102
|
+
* - **🏃♂️ Race Conditions**: User clicks fast, requests arrive out of order? `switchMap` cancels old requests.
|
|
103
|
+
* - **🔄 Retry Logic**: Network failed? Built-in retry operators handle backoff and error recovery.
|
|
104
|
+
* - **⚡ Backpressure**: Producer too fast for consumer? Built-in flow control prevents memory bloat.
|
|
105
|
+
* - **🧪 Testing**: Complex async flows become simple to test with predictable, pure operators.
|
|
106
|
+
* - **🎯 Composability**: Mix and match operators like Lego blocks to build exactly what you need.
|
|
107
|
+
*
|
|
108
|
+
* ## ✨ Feature Highlights
|
|
109
|
+
* - **Unified push + pull** – use callbacks *or* `for await … of` on the same
|
|
110
|
+
* stream.
|
|
111
|
+
* - **Cold by default** – each subscriber gets an independent execution (great
|
|
112
|
+
* for predictable side‑effects).
|
|
113
|
+
* - **Deterministic teardown** – return a function/`unsubscribe`/`[Symbol.dispose]`
|
|
114
|
+
* and it *always* runs once, even if the observable errors synchronously.
|
|
115
|
+
* - **Back‑pressure helper** – `pull()` converts to an `AsyncGenerator` backed
|
|
116
|
+
* by `ReadableStream` so the producer slows down when the consumer lags.
|
|
117
|
+
* - **Tiny surface** – <3 kB min+gzip of logic; treeshakes cleanly.
|
|
118
|
+
* - **Rich operator library** – functional composition via `pipe()` with full
|
|
119
|
+
* type safety and backpressure support.
|
|
120
|
+
* - **EventBus & EventDispatcher** – built-in multicast event buses for pub/sub patterns.
|
|
121
|
+
* - **Advanced error handling** – 4-mode error handling system (pass-through, ignore, throw, manual).
|
|
122
|
+
* - **High-performance operators** – Web Streams-based operators with pre-compiled error handling.
|
|
123
|
+
*
|
|
124
|
+
* ## What Makes This Observables Implementation Special
|
|
125
|
+
*
|
|
126
|
+
* `@okikio/observables` isn't just another Observable library. It's designed to be:
|
|
127
|
+
*
|
|
128
|
+
* - **Beginner-friendly**: If you know `Array.map()`, you already understand operators
|
|
129
|
+
* - **Performance-first**: Built on Web Streams with pre-compiled error handling for speed
|
|
130
|
+
* - **TypeScript-native**: Full type safety with intelligent inference
|
|
131
|
+
* - **Standards-compliant**: Follows the TC39 Observable proposal for future compatibility
|
|
132
|
+
* - **Tiny but complete**: <3KB but includes everything you need for real apps
|
|
133
|
+
* - **Error-resilient**: 4 different error handling modes for every situation
|
|
134
|
+
*
|
|
135
|
+
* ## Getting Started: Your First Observable
|
|
136
|
+
*
|
|
137
|
+
* Let's start simple. Here's how to create and use an Observable:
|
|
138
|
+
*
|
|
139
|
+
* @example Creating Observables
|
|
140
|
+
* ```ts
|
|
141
|
+
* import { Observable } from './observable.ts';
|
|
142
|
+
*
|
|
143
|
+
* // Method 1: From scratch (like creating a custom Promise)
|
|
144
|
+
* const timer = new Observable(observer => {
|
|
145
|
+
* let count = 0;
|
|
146
|
+
* const id = setInterval(() => {
|
|
147
|
+
* observer.next(count++); // Send values
|
|
148
|
+
* if (count > 5) {
|
|
149
|
+
* observer.complete(); // Finish
|
|
150
|
+
* }
|
|
151
|
+
* }, 1000);
|
|
152
|
+
*
|
|
153
|
+
* // Return cleanup function (like Promise.finally)
|
|
154
|
+
* return () => clearInterval(id);
|
|
155
|
+
* });
|
|
156
|
+
*
|
|
157
|
+
* // Method 2: From existing values (like Promise.resolve)
|
|
158
|
+
* const numbers = Observable.of(1, 2, 3, 4, 5);
|
|
159
|
+
*
|
|
160
|
+
* // Method 3: From promises, arrays, or other async sources
|
|
161
|
+
* const apiData = Observable.from(fetch('/api/users'));
|
|
162
|
+
* const listData = Observable.from([1, 2, 3, 4, 5]);
|
|
163
|
+
* ```
|
|
164
|
+
*
|
|
165
|
+
* @example Consuming Observables
|
|
166
|
+
* ```ts
|
|
167
|
+
* // Method 1: Subscribe with callbacks (like Promise.then)
|
|
168
|
+
* const subscription = timer.subscribe({
|
|
169
|
+
* next: value => console.log('Got:', value), // Handle each value
|
|
170
|
+
* error: err => console.error('Error:', err), // Handle errors
|
|
171
|
+
* complete: () => console.log('All done!') // Handle completion
|
|
172
|
+
* });
|
|
173
|
+
*
|
|
174
|
+
* // Don't forget to clean up! (or you'll get memory leaks)
|
|
175
|
+
* subscription.unsubscribe();
|
|
176
|
+
*
|
|
177
|
+
* // Method 2: Use modern "using" syntax for automatic cleanup
|
|
178
|
+
* {
|
|
179
|
+
* using sub = timer.subscribe(value => console.log(value));
|
|
180
|
+
* // Code here...
|
|
181
|
+
* } // Automatically unsubscribed at block end!
|
|
182
|
+
*
|
|
183
|
+
* // Method 3: Async iteration (like for-await with arrays)
|
|
184
|
+
* for await (const value of timer) {
|
|
185
|
+
* console.log('Value:', value);
|
|
186
|
+
* if (value > 3) break; // Stop early if needed
|
|
187
|
+
* } // Automatically cleaned up when loop exits
|
|
188
|
+
* ```
|
|
189
|
+
*
|
|
190
|
+
* That's it! You now know the basics. But the real power comes from **operators**...
|
|
191
|
+
*
|
|
192
|
+
* ## Operators
|
|
193
|
+
*
|
|
194
|
+
* If you've used `Array.map()` or `Array.filter()`, you already understand operators.
|
|
195
|
+
* They're just like array methods, but for data that arrives over time:
|
|
196
|
+
*
|
|
197
|
+
* ```ts
|
|
198
|
+
* // With arrays (data you already have):
|
|
199
|
+
* [1, 2, 3, 4, 5]
|
|
200
|
+
* .filter(x => x % 2 === 0) // Keep even numbers: [2, 4]
|
|
201
|
+
* .map(x => x * 10) // Multiply by 10: [20, 40]
|
|
202
|
+
* .slice(0, 1) // Take first: [20]
|
|
203
|
+
*
|
|
204
|
+
* // With Observables (data arriving over time):
|
|
205
|
+
* pipe(
|
|
206
|
+
* numberStream,
|
|
207
|
+
* filter(x => x % 2 === 0), // Keep even numbers
|
|
208
|
+
* map(x => x * 10), // Multiply by 10
|
|
209
|
+
* take(1) // Take first
|
|
210
|
+
* )
|
|
211
|
+
* ```
|
|
212
|
+
*
|
|
213
|
+
* The difference? Arrays process everything at once. Observables process data
|
|
214
|
+
* piece-by-piece as it arrives, without loading everything into memory.
|
|
215
|
+
*
|
|
216
|
+
* @example Real-World Example: User Search
|
|
217
|
+
* ```ts
|
|
218
|
+
* import { pipe, debounce, filter, switchMap, map } from './helpers/mod.ts';
|
|
219
|
+
*
|
|
220
|
+
* // Transform raw input events into search results
|
|
221
|
+
* const searchResults = pipe(
|
|
222
|
+
* userInput, // Raw keystrokes
|
|
223
|
+
* debounce(300), // Wait for typing pause
|
|
224
|
+
* filter(query => query.length > 2), // Skip short queries
|
|
225
|
+
* switchMap(query => // Cancel old searches
|
|
226
|
+
* Observable.from(fetch(`/search?q=${query}`))
|
|
227
|
+
* ),
|
|
228
|
+
* map(response => response.json()) // Parse JSON
|
|
229
|
+
* );
|
|
230
|
+
*
|
|
231
|
+
* // Use the results
|
|
232
|
+
* searchResults.subscribe(results => {
|
|
233
|
+
* updateUI(results);
|
|
234
|
+
* });
|
|
235
|
+
* ```
|
|
236
|
+
*
|
|
237
|
+
* Each operator transforms the data in some way, and you can chain as many as you need.
|
|
238
|
+
* It's like building a data processing pipeline where each step does one thing well.
|
|
239
|
+
*
|
|
240
|
+
* ## Operator Categories
|
|
241
|
+
*
|
|
242
|
+
* Operators enables powerful functional composition patterns using the `pipe()` function.
|
|
243
|
+
* All operators are **type-safe**, **tree-shakable**, **support automatic backpressure**, and feature
|
|
244
|
+
* **advanced error handling** with 4 distinct modes: pass-through, ignore, throw, and manual.
|
|
245
|
+
*
|
|
246
|
+
* There are many operators, but they fall into clear categories. You don't need to learn
|
|
247
|
+
* them all at once, start with the ones you need:
|
|
248
|
+
*
|
|
249
|
+
* ### 🔄 **Transformation**: Change data as it flows
|
|
250
|
+
* ```ts
|
|
251
|
+
* pipe(
|
|
252
|
+
* numbers,
|
|
253
|
+
* map(x => x * 2), // Transform each value: 1 → 2, 2 → 4
|
|
254
|
+
* scan((sum, x) => sum + x) // Running total: 2, 6, 12, 20...
|
|
255
|
+
* )
|
|
256
|
+
* ```
|
|
257
|
+
* - `map(fn)` – Transform each value (like `Array.map`)
|
|
258
|
+
* - `scan(fn, seed)` – Running accumulation (like `Array.reduce` over time)
|
|
259
|
+
* - `batch(size)` – Group values into arrays
|
|
260
|
+
* - `toArray()` – Collect everything into one array
|
|
261
|
+
*
|
|
262
|
+
* ### 🚰 **Filtering**: Control what data gets through
|
|
263
|
+
* ```ts
|
|
264
|
+
* pipe(
|
|
265
|
+
* allClicks,
|
|
266
|
+
* filter(event => event.target.matches('button')), // Only button clicks
|
|
267
|
+
* take(5) // Stop after 5 clicks
|
|
268
|
+
* )
|
|
269
|
+
* ```
|
|
270
|
+
* - `filter(predicate)` – Keep values that pass a test (like `Array.filter`)
|
|
271
|
+
* - `take(count)` – Take only the first N values
|
|
272
|
+
* - `drop(count)` – Skip the first N values
|
|
273
|
+
* - `find(predicate)` – Find first matching value and stop
|
|
274
|
+
*
|
|
275
|
+
* ### ⏰ **Timing**: Control when things happen
|
|
276
|
+
* ```ts
|
|
277
|
+
* pipe(
|
|
278
|
+
* keystrokes,
|
|
279
|
+
* debounce(300), // Wait 300ms after last keystroke
|
|
280
|
+
* delay(100) // Add 100ms delay to everything
|
|
281
|
+
* )
|
|
282
|
+
* ```
|
|
283
|
+
* - `debounce(ms)` – Wait for silence before emitting
|
|
284
|
+
* - `throttle(ms)` – Limit emission rate
|
|
285
|
+
* - `delay(ms)` – Delay all emissions by time
|
|
286
|
+
* - `timeout(ms)` – Cancel if nothing happens within time
|
|
287
|
+
*
|
|
288
|
+
* ### 🔀 **Combination**: Merge multiple streams
|
|
289
|
+
* ```ts
|
|
290
|
+
* pipe(
|
|
291
|
+
* searchQueries,
|
|
292
|
+
* switchMap(query => // For each query...
|
|
293
|
+
* fetch(`/search?q=${query}`) // ...start a request (cancel previous)
|
|
294
|
+
* )
|
|
295
|
+
* )
|
|
296
|
+
* ```
|
|
297
|
+
* - `mergeMap(fn)` – Start multiple operations, merge results
|
|
298
|
+
* - `concatMap(fn)` – Start operations one at a time
|
|
299
|
+
* - `switchMap(fn)` – Cancel previous operation when new one starts
|
|
300
|
+
*
|
|
301
|
+
* ### ⚠️ **Error Handling**: Deal with things going wrong
|
|
302
|
+
* ```ts
|
|
303
|
+
* pipe(
|
|
304
|
+
* riskyOperations,
|
|
305
|
+
* catchErrors('fallback'), // Replace errors with fallback
|
|
306
|
+
* ignoreErrors() // Skip errors, keep going
|
|
307
|
+
* )
|
|
308
|
+
* ```
|
|
309
|
+
* - `catchErrors(fallback)` – Replace errors with fallback values
|
|
310
|
+
* - `ignoreErrors()` – Skip errors silently
|
|
311
|
+
* - `tapError(fn)` – Log errors without changing the stream
|
|
312
|
+
* - `mapErrors(fn)` – Transform error values
|
|
313
|
+
*
|
|
314
|
+
* ### 🔧 **Utilities**: Side effects and debugging
|
|
315
|
+
* ```ts
|
|
316
|
+
* pipe(
|
|
317
|
+
* dataStream,
|
|
318
|
+
* tap(x => console.log('Debug:', x)), // Log without changing values
|
|
319
|
+
* tap(x => analytics.track(x)) // Send to analytics
|
|
320
|
+
* )
|
|
321
|
+
* ```
|
|
322
|
+
* - `tap(fn)` – Run side effects without changing values
|
|
323
|
+
*
|
|
324
|
+
* ## Real-World Examples: See It In Action
|
|
325
|
+
*
|
|
326
|
+
* Let's see how these operators solve actual problems you face every day:
|
|
327
|
+
*
|
|
328
|
+
* @example Smart Search with Debouncing
|
|
329
|
+
* ```ts
|
|
330
|
+
* import { pipe, debounce, filter, switchMap, map, catchErrors } from './helpers/mod.ts';
|
|
331
|
+
*
|
|
332
|
+
* // Problem: User types fast, you don't want to spam the API
|
|
333
|
+
* // Solution: Debounce + cancel previous requests
|
|
334
|
+
* const searchResults = pipe(
|
|
335
|
+
* searchInput,
|
|
336
|
+
* debounce(300), // Wait for typing pause
|
|
337
|
+
* filter(query => query.length > 2), // Skip short queries
|
|
338
|
+
* switchMap(query => // Cancel old requests automatically
|
|
339
|
+
* pipe(
|
|
340
|
+
* Observable.from(fetch(`/search?q=${query}`)),
|
|
341
|
+
* map(res => res.json()),
|
|
342
|
+
* catchErrors([]) // Return empty array on error
|
|
343
|
+
* )
|
|
344
|
+
* )
|
|
345
|
+
* );
|
|
346
|
+
*
|
|
347
|
+
* searchResults.subscribe(results => updateUI(results));
|
|
348
|
+
* ```
|
|
349
|
+
*
|
|
350
|
+
* @example Real-Time Data Dashboard
|
|
351
|
+
* ```ts
|
|
352
|
+
* import { pipe, filter, scan, throttle, batch } from './helpers/mod.ts';
|
|
353
|
+
*
|
|
354
|
+
* // Problem: WebSocket sends lots of data, UI can't keep up
|
|
355
|
+
* // Solution: Filter, accumulate, and throttle updates
|
|
356
|
+
* const dashboardData = pipe(
|
|
357
|
+
* webSocketEvents,
|
|
358
|
+
* filter(event => event.type === 'metric'), // Only metric events
|
|
359
|
+
* scan((acc, event) => ({ // Build running totals
|
|
360
|
+
* ...acc,
|
|
361
|
+
* total: acc.total + event.value,
|
|
362
|
+
* count: acc.count + 1,
|
|
363
|
+
* average: (acc.total + event.value) / (acc.count + 1)
|
|
364
|
+
* }), { total: 0, count: 0, average: 0 }),
|
|
365
|
+
* throttle(1000) // Update UI max once per second
|
|
366
|
+
* );
|
|
367
|
+
*
|
|
368
|
+
* dashboardData.subscribe(stats => updateDashboard(stats));
|
|
369
|
+
* ```
|
|
370
|
+
*
|
|
371
|
+
* @example File Upload with Progress
|
|
372
|
+
* ```ts
|
|
373
|
+
* import { pipe, map, scan, tap } from './helpers/mod.ts';
|
|
374
|
+
*
|
|
375
|
+
* // Problem: Show upload progress and handle completion
|
|
376
|
+
* // Solution: Transform progress events into UI updates
|
|
377
|
+
* const uploadProgress = pipe(
|
|
378
|
+
* fileUploadEvents,
|
|
379
|
+
* map(event => ({ // Extract useful info
|
|
380
|
+
* loaded: event.loaded,
|
|
381
|
+
* total: event.total,
|
|
382
|
+
* percent: Math.round((event.loaded / event.total) * 100)
|
|
383
|
+
* })),
|
|
384
|
+
* tap(progress => updateProgressBar(progress.percent)), // Update UI
|
|
385
|
+
* filter(progress => progress.percent === 100), // Only completion
|
|
386
|
+
* map(() => 'Upload complete!') // Success message
|
|
387
|
+
* );
|
|
388
|
+
*
|
|
389
|
+
* uploadProgress.subscribe(message => showSuccess(message));
|
|
390
|
+
* ```
|
|
391
|
+
*
|
|
392
|
+
* @example Background Data Sync
|
|
393
|
+
* ```ts
|
|
394
|
+
* import { pipe, mergeMap, delay, catchErrors, tap } from './helpers/mod.ts';
|
|
395
|
+
*
|
|
396
|
+
* // Problem: Sync data in background, retry on failure, don't overwhelm server
|
|
397
|
+
* // Solution: Batch processing with concurrency control and error recovery
|
|
398
|
+
* const syncResults = pipe(
|
|
399
|
+
* pendingItems,
|
|
400
|
+
* batch(10), // Process 10 items at a time
|
|
401
|
+
* mergeMap(batch => // Process up to 3 batches concurrently
|
|
402
|
+
* pipe(
|
|
403
|
+
* Observable.from(syncBatch(batch)),
|
|
404
|
+
* delay(100), // Be nice to the server
|
|
405
|
+
* catchErrors(null), // Don't fail everything on one error
|
|
406
|
+
* tap(result => updateSyncStatus(result))
|
|
407
|
+
* ),
|
|
408
|
+
* 3 // Max 3 concurrent operations
|
|
409
|
+
* ),
|
|
410
|
+
* filter(result => result !== null) // Skip failed syncs
|
|
411
|
+
* );
|
|
412
|
+
*
|
|
413
|
+
* syncResults.subscribe(result => logSyncSuccess(result));
|
|
414
|
+
* ```
|
|
415
|
+
*
|
|
416
|
+
* Notice the pattern? Each operator does one job well, and you combine them to solve
|
|
417
|
+
* complex problems. It's like having a Swiss Army knife for async data.
|
|
418
|
+
*
|
|
419
|
+
* ## Building Your Own Operators
|
|
420
|
+
*
|
|
421
|
+
* Sometimes the built-in operators aren't enough. That's fine! You can build your own.
|
|
422
|
+
* Think of it like creating custom functions, but for streams:
|
|
423
|
+
*
|
|
424
|
+
* @example Simple Custom Operator
|
|
425
|
+
* ```ts
|
|
426
|
+
* import { createOperator } from './helpers/mod.ts';
|
|
427
|
+
*
|
|
428
|
+
* // Create a "double" operator (like multiplying every array element by 2)
|
|
429
|
+
* function double<T extends number>() {
|
|
430
|
+
* return createOperator<T, T>({
|
|
431
|
+
* name: 'double', // For debugging
|
|
432
|
+
* transform(value, controller) {
|
|
433
|
+
* controller.enqueue(value * 2); // Send doubled value
|
|
434
|
+
* }
|
|
435
|
+
* });
|
|
436
|
+
* }
|
|
437
|
+
*
|
|
438
|
+
* // Use it like any other operator
|
|
439
|
+
* pipe(
|
|
440
|
+
* Observable.of(1, 2, 3),
|
|
441
|
+
* double()
|
|
442
|
+
* ).subscribe(console.log); // 2, 4, 6
|
|
443
|
+
* ```
|
|
444
|
+
*
|
|
445
|
+
* @example Stateful Custom Operator
|
|
446
|
+
* ```ts
|
|
447
|
+
* import { createStatefulOperator } from './helpers/mod.ts';
|
|
448
|
+
*
|
|
449
|
+
* // Create a "moving average" operator that remembers previous values
|
|
450
|
+
* function movingAverage(windowSize: number) {
|
|
451
|
+
* return createStatefulOperator<number, number, number[]>({
|
|
452
|
+
* name: 'movingAverage',
|
|
453
|
+
* createState: () => [], // Start with empty array
|
|
454
|
+
*
|
|
455
|
+
* transform(value, arr, controller) {
|
|
456
|
+
* arr.push(value); // Add new value
|
|
457
|
+
* if (arr.length > windowSize) {
|
|
458
|
+
* arr.shift(); // Remove old values
|
|
459
|
+
* }
|
|
460
|
+
*
|
|
461
|
+
* // Calculate and emit average
|
|
462
|
+
* const avg = arr.reduce((sum, n) => sum + n, 0) / arr.length;
|
|
463
|
+
* controller.enqueue(avg);
|
|
464
|
+
* }
|
|
465
|
+
* });
|
|
466
|
+
* }
|
|
467
|
+
*
|
|
468
|
+
* // Use it to smooth noisy sensor data
|
|
469
|
+
* pipe(
|
|
470
|
+
* noisySensorData,
|
|
471
|
+
* movingAverage(5) // 5-value moving average
|
|
472
|
+
* ).subscribe(smoothValue => updateDisplay(smoothValue));
|
|
473
|
+
* ```
|
|
474
|
+
*
|
|
475
|
+
* The beauty of this system is that your custom operators work exactly like the built-in ones.
|
|
476
|
+
* You can combine them, test them separately, and reuse them across projects.
|
|
477
|
+
*
|
|
478
|
+
* ## Error Handling: When Things Go Wrong
|
|
479
|
+
*
|
|
480
|
+
* Real-world data is messy. Networks fail, users input bad data, APIs return errors.
|
|
481
|
+
* This library gives you **four ways** to handle errors, so you can choose what makes
|
|
482
|
+
* sense for your situation:
|
|
483
|
+
*
|
|
484
|
+
* ```ts
|
|
485
|
+
* // 1. "pass-through" (default): Errors become values in the stream
|
|
486
|
+
* const safeParser = createOperator({
|
|
487
|
+
* errorMode: 'pass-through', // Errors become ObservableError values
|
|
488
|
+
* transform(jsonString, controller) {
|
|
489
|
+
* controller.enqueue(JSON.parse(jsonString)); // If this fails, error flows as value
|
|
490
|
+
* }
|
|
491
|
+
* });
|
|
492
|
+
*
|
|
493
|
+
* // 2. "ignore": Skip errors silently
|
|
494
|
+
* const lenientParser = createOperator({
|
|
495
|
+
* errorMode: 'ignore', // Errors are silently skipped
|
|
496
|
+
* transform(jsonString, controller) {
|
|
497
|
+
* controller.enqueue(JSON.parse(jsonString)); // Bad JSON just disappears
|
|
498
|
+
* }
|
|
499
|
+
* });
|
|
500
|
+
*
|
|
501
|
+
* // 3. "throw": Stop everything on first error
|
|
502
|
+
* const strictParser = createOperator({
|
|
503
|
+
* errorMode: 'throw', // Errors terminate the stream
|
|
504
|
+
* transform(jsonString, controller) {
|
|
505
|
+
* controller.enqueue(JSON.parse(jsonString)); // Bad JSON kills the stream
|
|
506
|
+
* }
|
|
507
|
+
* });
|
|
508
|
+
*
|
|
509
|
+
* // 4. "manual": You handle everything yourself
|
|
510
|
+
* const customParser = createOperator({
|
|
511
|
+
* errorMode: 'manual', // You're in control
|
|
512
|
+
* transform(jsonString, controller) {
|
|
513
|
+
* try {
|
|
514
|
+
* controller.enqueue(JSON.parse(jsonString));
|
|
515
|
+
* } catch (err) {
|
|
516
|
+
* // Your custom error logic here
|
|
517
|
+
* controller.enqueue({ error: err.message, input: jsonString });
|
|
518
|
+
* }
|
|
519
|
+
* }
|
|
520
|
+
* });
|
|
521
|
+
* ```
|
|
522
|
+
*
|
|
523
|
+
* **When to use which mode?**
|
|
524
|
+
* - **pass-through**: When you want to handle errors downstream (most common)
|
|
525
|
+
* - **ignore**: When bad data should just be filtered out
|
|
526
|
+
* - **throw**: When any error means the whole operation failed
|
|
527
|
+
* - **manual**: When you need custom error handling logic
|
|
528
|
+
*
|
|
529
|
+
* ## EventBus: For Pub/Sub Patterns
|
|
530
|
+
*
|
|
531
|
+
* Sometimes you need **one-to-many communication**, like a chat app where one message
|
|
532
|
+
* goes to multiple users, or a shopping cart that updates multiple UI components.
|
|
533
|
+
* That's where EventBus comes in:
|
|
534
|
+
*
|
|
535
|
+
* @example Simple EventBus
|
|
536
|
+
* ```ts
|
|
537
|
+
* import { EventBus } from './events.ts';
|
|
538
|
+
*
|
|
539
|
+
* // Create a bus for chat messages
|
|
540
|
+
* const chatBus = new EventBus<string>();
|
|
541
|
+
*
|
|
542
|
+
* // Multiple components can listen
|
|
543
|
+
* chatBus.events.subscribe(msg => updateChatWindow(msg));
|
|
544
|
+
* chatBus.events.subscribe(msg => updateNotificationBadge(msg));
|
|
545
|
+
* chatBus.events.subscribe(msg => logMessage(msg));
|
|
546
|
+
*
|
|
547
|
+
* // One emit reaches everyone
|
|
548
|
+
* chatBus.emit('Hello everyone!');
|
|
549
|
+
* // All three subscribers receive the message
|
|
550
|
+
*
|
|
551
|
+
* chatBus.close(); // Clean up when done
|
|
552
|
+
* ```
|
|
553
|
+
*
|
|
554
|
+
* @example EventBus with async iteration
|
|
555
|
+
* ```ts
|
|
556
|
+
* import { EventBus } from './events.ts';
|
|
557
|
+
*
|
|
558
|
+
* const statusBus = new EventBus<{ status: string; data: any }>();
|
|
559
|
+
*
|
|
560
|
+
* // Listen using for-await (great for async processing)
|
|
561
|
+
* async function handleStatusUpdates() {
|
|
562
|
+
* for await (const update of statusBus.events) {
|
|
563
|
+
* console.log('Status changed:', update.status);
|
|
564
|
+
*
|
|
565
|
+
* if (update.status === 'error') {
|
|
566
|
+
* await handleError(update.data);
|
|
567
|
+
* } else if (update.status === 'complete') {
|
|
568
|
+
* await finalizeProcess(update.data);
|
|
569
|
+
* break; // Exit the loop
|
|
570
|
+
* }
|
|
571
|
+
* }
|
|
572
|
+
* }
|
|
573
|
+
*
|
|
574
|
+
* // Start listening
|
|
575
|
+
* handleStatusUpdates();
|
|
576
|
+
*
|
|
577
|
+
* // Emit updates from anywhere in your app
|
|
578
|
+
* statusBus.emit({ status: 'processing', data: { progress: 50 } });
|
|
579
|
+
* statusBus.emit({ status: 'complete', data: { result: 'success' } });
|
|
580
|
+
* ```
|
|
581
|
+
*
|
|
582
|
+
* @example EventBus with operators
|
|
583
|
+
* ```ts
|
|
584
|
+
* import { EventBus } from './events.ts';
|
|
585
|
+
* import { pipe, filter, map, debounce } from './helpers/mod.ts';
|
|
586
|
+
*
|
|
587
|
+
* const clickBus = new EventBus<{ x: number; y: number; target: string }>();
|
|
588
|
+
*
|
|
589
|
+
* // Process clicks with operators
|
|
590
|
+
* const buttonClicks = pipe(
|
|
591
|
+
* clickBus.events,
|
|
592
|
+
* filter(click => click.target === 'button'), // Only button clicks
|
|
593
|
+
* debounce(100), // Prevent double-clicks
|
|
594
|
+
* map(click => ({ ...click, timestamp: Date.now() })) // Add timestamp
|
|
595
|
+
* );
|
|
596
|
+
*
|
|
597
|
+
* buttonClicks.subscribe(click => {
|
|
598
|
+
* console.log('Button clicked at', click.x, click.y);
|
|
599
|
+
* });
|
|
600
|
+
*
|
|
601
|
+
* // Emit clicks (maybe from a global click handler)
|
|
602
|
+
* document.addEventListener('click', (e) => {
|
|
603
|
+
* clickBus.emit({
|
|
604
|
+
* x: e.clientX,
|
|
605
|
+
* y: e.clientY,
|
|
606
|
+
* target: e.target.tagName.toLowerCase()
|
|
607
|
+
* });
|
|
608
|
+
* });
|
|
609
|
+
* ```
|
|
610
|
+
*
|
|
611
|
+
* @example Typed EventDispatcher
|
|
612
|
+
* ```ts
|
|
613
|
+
* import { createEventDispatcher } from './events.ts';
|
|
614
|
+
*
|
|
615
|
+
* // Define your event types (TypeScript ensures you use them correctly)
|
|
616
|
+
* interface AppEvents {
|
|
617
|
+
* userLogin: { userId: string; timestamp: number };
|
|
618
|
+
* userLogout: { userId: string };
|
|
619
|
+
* cartUpdate: { items: number; total: number };
|
|
620
|
+
* notification: { message: string; type: 'info' | 'warning' | 'error' };
|
|
621
|
+
* }
|
|
622
|
+
*
|
|
623
|
+
* const events = createEventDispatcher<AppEvents>();
|
|
624
|
+
*
|
|
625
|
+
* // Type-safe event emission
|
|
626
|
+
* events.emit('userLogin', { userId: '123', timestamp: Date.now() });
|
|
627
|
+
* events.emit('cartUpdate', { items: 3, total: 29.99 });
|
|
628
|
+
* events.emit('notification', { message: 'Welcome!', type: 'info' });
|
|
629
|
+
*
|
|
630
|
+
* // Type-safe event handling - listen to specific events
|
|
631
|
+
* events.on('userLogin', (data) => {
|
|
632
|
+
* analytics.track('login', data.userId); // TypeScript knows data.userId exists
|
|
633
|
+
* console.log('User logged in at', new Date(data.timestamp));
|
|
634
|
+
* });
|
|
635
|
+
*
|
|
636
|
+
* events.on('cartUpdate', (data) => {
|
|
637
|
+
* updateCartIcon(data.items); // TypeScript knows data.items is a number
|
|
638
|
+
* updateCartTotal(data.total);
|
|
639
|
+
* });
|
|
640
|
+
*
|
|
641
|
+
* // Listen to ALL events (useful for debugging or logging)
|
|
642
|
+
* events.events.subscribe(event => {
|
|
643
|
+
* console.log('Event:', event.type, 'Data:', event.payload);
|
|
644
|
+
* });
|
|
645
|
+
*
|
|
646
|
+
* // Use async iteration for event processing
|
|
647
|
+
* async function processNotifications() {
|
|
648
|
+
* for await (const event of events.events) {
|
|
649
|
+
* if (event.type === 'notification') {
|
|
650
|
+
* await showNotification(event.payload.message, event.payload.type);
|
|
651
|
+
* }
|
|
652
|
+
* }
|
|
653
|
+
* }
|
|
654
|
+
* ```
|
|
655
|
+
*
|
|
656
|
+
* @example Advanced EventBus patterns
|
|
657
|
+
* ```ts
|
|
658
|
+
* import { EventBus, withReplay, waitForEvent } from './events.ts';
|
|
659
|
+
*
|
|
660
|
+
* // Create a bus that replays the last 5 events to new subscribers
|
|
661
|
+
* const statusBus = new EventBus<string>();
|
|
662
|
+
* const replayableStatus = withReplay(statusBus.events, { count: 5 });
|
|
663
|
+
*
|
|
664
|
+
* // Emit some events
|
|
665
|
+
* statusBus.emit('initializing');
|
|
666
|
+
* statusBus.emit('loading data');
|
|
667
|
+
* statusBus.emit('processing');
|
|
668
|
+
*
|
|
669
|
+
* // New subscribers get the last 5 events immediately
|
|
670
|
+
* replayableStatus.subscribe(status => {
|
|
671
|
+
* console.log('Status:', status); // Will log all 3 previous events first
|
|
672
|
+
* });
|
|
673
|
+
*
|
|
674
|
+
* // Wait for a specific event (useful for async coordination)
|
|
675
|
+
* async function waitForCompletion() {
|
|
676
|
+
* try {
|
|
677
|
+
* const result = await waitForEvent(
|
|
678
|
+
* { events: statusBus.events },
|
|
679
|
+
* 'complete',
|
|
680
|
+
* { signal: AbortSignal.timeout(5000) } // 5 second timeout
|
|
681
|
+
* );
|
|
682
|
+
* console.log('Operation completed:', result);
|
|
683
|
+
* } catch (error) {
|
|
684
|
+
* console.log('Timed out or aborted');
|
|
685
|
+
* }
|
|
686
|
+
* }
|
|
687
|
+
*
|
|
688
|
+
* waitForCompletion();
|
|
689
|
+
* statusBus.emit('complete'); // This will resolve the waitForEvent promise
|
|
690
|
+
* ```
|
|
691
|
+
*
|
|
692
|
+
* @example EventBus resource management
|
|
693
|
+
* ```ts
|
|
694
|
+
* import { EventBus } from './events.ts';
|
|
695
|
+
*
|
|
696
|
+
* // EventBus supports using/await using for automatic cleanup
|
|
697
|
+
* {
|
|
698
|
+
* using messageBus = new EventBus<string>();
|
|
699
|
+
*
|
|
700
|
+
* // Set up listeners
|
|
701
|
+
* messageBus.events.subscribe(msg => console.log('Received:', msg));
|
|
702
|
+
*
|
|
703
|
+
* // Use the bus
|
|
704
|
+
* messageBus.emit('Hello world!');
|
|
705
|
+
*
|
|
706
|
+
* } // Bus automatically closed and all resources cleaned up
|
|
707
|
+
*
|
|
708
|
+
* // Also works with async using
|
|
709
|
+
* async function setupEventSystem() {
|
|
710
|
+
* await using eventSystem = new EventBus<any>();
|
|
711
|
+
*
|
|
712
|
+
* // Set up complex event handling
|
|
713
|
+
* eventSystem.events.subscribe(processEvents);
|
|
714
|
+
*
|
|
715
|
+
* // Do async work...
|
|
716
|
+
* await someAsyncOperation();
|
|
717
|
+
*
|
|
718
|
+
* } // Async cleanup happens automatically
|
|
719
|
+
* ```
|
|
720
|
+
*
|
|
721
|
+
* **When to use EventBus vs Observable?**
|
|
722
|
+
* - **Observable**: One-to-one, like transforming API data or handling user input
|
|
723
|
+
* - **EventBus**: One-to-many, like app-wide notifications, state updates, or cross-component communication
|
|
724
|
+
* - **EventDispatcher**: Type-safe pub/sub with multiple event types in one system
|
|
725
|
+
*
|
|
726
|
+
* **EventBus Consumption Patterns:**
|
|
727
|
+
* - **subscribe()**: For imperative event handling with callbacks
|
|
728
|
+
* - **for await**: For sequential async processing of events
|
|
729
|
+
* - **pipe() + operators**: For transforming and filtering events
|
|
730
|
+
* - **waitForEvent()**: For waiting for specific events in async functions
|
|
731
|
+
*
|
|
732
|
+
* ## Performance: Built for Speed
|
|
733
|
+
*
|
|
734
|
+
* This isn't just a learning library, it's built for production apps that need to handle
|
|
735
|
+
* lots of data efficiently:
|
|
736
|
+
*
|
|
737
|
+
* ### Web Streams Foundation
|
|
738
|
+
* Under the hood, operators use **Web Streams**, which gives you:
|
|
739
|
+
* - **Native backpressure**: Fast producers don't overwhelm slow consumers
|
|
740
|
+
* - **Memory efficiency**: Process data piece-by-piece, not all at once
|
|
741
|
+
* - **Browser optimization**: Built-in browser optimizations kick in
|
|
742
|
+
*
|
|
743
|
+
* ### Pre-compiled Error Handling
|
|
744
|
+
* Instead of checking error modes on every piece of data (slow), we generate
|
|
745
|
+
* optimized functions for each error mode (fast):
|
|
746
|
+
*
|
|
747
|
+
* | Error Mode | Performance | When to Use |
|
|
748
|
+
* |------------|-------------|-------------|
|
|
749
|
+
* | `manual` | Fastest | Hot paths where you handle errors yourself |
|
|
750
|
+
* | `ignore` | Very fast | Filtering bad data |
|
|
751
|
+
* | `pass-through` | Fast | Error recovery, debugging |
|
|
752
|
+
* | `throw` | Good | Fail-fast validation |
|
|
753
|
+
*
|
|
754
|
+
* ### Memory Management
|
|
755
|
+
* - **Automatic cleanup**: `using` syntax and `Symbol.dispose` prevent leaks
|
|
756
|
+
* - **Circular buffer queues**: O(1) operations for high-throughput data
|
|
757
|
+
* - **Smart resource management**: Resources freed immediately when streams end
|
|
758
|
+
*
|
|
759
|
+
* @example Performance Tuning
|
|
760
|
+
* ```ts
|
|
761
|
+
* // For high-throughput data processing
|
|
762
|
+
* const optimized = pipe(
|
|
763
|
+
* highVolumeStream,
|
|
764
|
+
*
|
|
765
|
+
* // Use manual error mode for maximum speed
|
|
766
|
+
* createOperator({
|
|
767
|
+
* errorMode: 'manual',
|
|
768
|
+
* transform(chunk, controller) {
|
|
769
|
+
* try {
|
|
770
|
+
* controller.enqueue(processChunk(chunk));
|
|
771
|
+
* } catch (err) {
|
|
772
|
+
* logError(err); // Handle as needed
|
|
773
|
+
* }
|
|
774
|
+
* }
|
|
775
|
+
* }),
|
|
776
|
+
*
|
|
777
|
+
* batch(100), // Process in efficient batches
|
|
778
|
+
* mergeMap(batch => processBatch(batch), 3) // Limit concurrency
|
|
779
|
+
* );
|
|
780
|
+
*
|
|
781
|
+
* // For memory-constrained environments
|
|
782
|
+
* for await (const chunk of bigDataStream.pull({
|
|
783
|
+
* strategy: { highWaterMark: 8 } // Small buffer
|
|
784
|
+
* })) {
|
|
785
|
+
* await processLargeChunk(chunk);
|
|
786
|
+
* }
|
|
787
|
+
* ```
|
|
788
|
+
*
|
|
789
|
+
* ## Common Gotchas & How to Avoid Them
|
|
790
|
+
*
|
|
791
|
+
* Even with great tools, there are some things that can trip you up. Here's how to avoid them:
|
|
792
|
+
*
|
|
793
|
+
* **🔥 Memory Leaks**: The #1 Observable mistake
|
|
794
|
+
* ```ts
|
|
795
|
+
* // ❌ Bad: Creates memory leak
|
|
796
|
+
* const timer = new Observable(obs => {
|
|
797
|
+
* setInterval(() => obs.next(Date.now()), 1000);
|
|
798
|
+
* // Missing cleanup function!
|
|
799
|
+
* });
|
|
800
|
+
* timer.subscribe(console.log); // This will run forever
|
|
801
|
+
*
|
|
802
|
+
* // ✅ Good: Always provide cleanup
|
|
803
|
+
* const timer = new Observable(obs => {
|
|
804
|
+
* const id = setInterval(() => obs.next(Date.now()), 1000);
|
|
805
|
+
* return () => clearInterval(id); // Cleanup function
|
|
806
|
+
* });
|
|
807
|
+
* using sub = timer.subscribe(console.log); // Auto-cleanup with 'using'
|
|
808
|
+
* ```
|
|
809
|
+
*
|
|
810
|
+
* **🏁 Race Conditions**: When requests finish out of order
|
|
811
|
+
* ```ts
|
|
812
|
+
* // ❌ Bad: Last request might not be latest
|
|
813
|
+
* searchInput.subscribe(query => {
|
|
814
|
+
* fetch(`/search?q=${query}`)
|
|
815
|
+
* .then(response => response.json())
|
|
816
|
+
* .then(results => updateUI(results)); // Wrong results might appear!
|
|
817
|
+
* });
|
|
818
|
+
*
|
|
819
|
+
* // ✅ Good: Use switchMap to cancel old requests
|
|
820
|
+
* pipe(
|
|
821
|
+
* searchInput,
|
|
822
|
+
* switchMap(query => Observable.from(fetch(`/search?q=${query}`)))
|
|
823
|
+
* ).subscribe(response => updateUI(response));
|
|
824
|
+
* ```
|
|
825
|
+
*
|
|
826
|
+
* **❄️ Cold vs Hot Confusion**: Understanding when side effects happen
|
|
827
|
+
*
|
|
828
|
+
* Observable (cold): Side effect runs once per subscription.
|
|
829
|
+
* EventBus (hot): Single source shared among multiple subscribers.
|
|
830
|
+
*
|
|
831
|
+
* **🚧 Operator Limits**: TypeScript has recursion limits
|
|
832
|
+
*
|
|
833
|
+
* Break into smaller, reusable functions for complex pipelines.
|
|
834
|
+
*
|
|
835
|
+
* ## Getting Started: Your First Steps
|
|
836
|
+
*
|
|
837
|
+
* 1. **Start simple**: Convert a Promise to an Observable
|
|
838
|
+
* 2. **Add operators**: Try transformation and filtering first
|
|
839
|
+
* 3. **Handle timing**: Add debouncing to a search input
|
|
840
|
+
* 4. **Manage errors**: Use error catching for graceful degradation
|
|
841
|
+
* 5. **Combine streams**: Use switching operators for request cancellation
|
|
842
|
+
*
|
|
843
|
+
* The operators work just like array methods, if you know transformation and filtering,
|
|
844
|
+
* you're already halfway there. The real power comes from combining operators to
|
|
845
|
+
* solve complex async problems with simple, composable code.
|
|
846
|
+
*
|
|
847
|
+
* **Implementation Notes:**
|
|
848
|
+
* - Follows TC39 Observable proposal for future compatibility
|
|
849
|
+
* - Built on Web Streams for performance and native backpressure
|
|
850
|
+
* - Fully tree-shakable - import only what you use
|
|
851
|
+
* - Comprehensive TypeScript support with intelligent inference
|
|
852
|
+
* - Multiple error handling modes for different use cases
|
|
853
|
+
* - Extensive test suite ensuring reliability
|
|
854
|
+
*
|
|
855
|
+
* @module
|
|
856
|
+
*/
|
|
857
|
+
import "./_dnt.polyfills.js";
|
|
858
|
+
export * from "./observable.js";
|
|
859
|
+
export * from "./error.js";
|
|
860
|
+
export * from "./events.js";
|
|
861
|
+
export * from "./helpers/mod.js";
|