@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Okiki Ojo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
# @okikio/observables
|
|
2
|
+
|
|
3
|
+
[](https://github.com/okikio/observables/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@okikio/observables)
|
|
5
|
+
[](https://bundlejs.com/?q=@okikio/observables&treeshake=[{+Observable,+pipe,+map,+filter+}])
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
[Documentation](https://jsr.io/@okikio/observables) •
|
|
9
|
+
[npm](https://www.npmjs.com/package/@okikio/observables) •
|
|
10
|
+
[GitHub](https://github.com/okikio/observables#readme) • [License](./LICENSE)
|
|
11
|
+
|
|
12
|
+
<!-- [](https://bundlejs.com/?q=@okikio/observables&bundle "Check the total bundle size of @okikio/observables") -->
|
|
13
|
+
|
|
14
|
+
A **spec-faithful** yet ergonomic TC39-inspired Observable implementation that
|
|
15
|
+
gives you one consistent way to handle all async data in JavaScript.
|
|
16
|
+
|
|
17
|
+
Built for Deno v2+, Node, Bun, and modern browsers, `@okikio/observables` keeps
|
|
18
|
+
the TC39 Observable proposal's mental model while adding the parts that make
|
|
19
|
+
day-to-day app code easier to write:
|
|
20
|
+
|
|
21
|
+
- **Observable pipelines that feel familiar** if you already know `Array.map()`
|
|
22
|
+
and `Array.filter()`
|
|
23
|
+
- **Web Streams-powered backpressure** so fast producers do not silently bloat
|
|
24
|
+
memory
|
|
25
|
+
- **Deterministic cleanup** via `unsubscribe()`, `using`, and `Symbol.dispose`
|
|
26
|
+
- **Built-in event primitives** for pub/sub and type-safe event dispatch
|
|
27
|
+
- **Four error modes** so you can choose between recovery, filtering, and
|
|
28
|
+
fail-fast behavior
|
|
29
|
+
|
|
30
|
+
**Start here:** [Installation](#installation) • [Quick Start](#quick-start) •
|
|
31
|
+
[API](#api) • [Advanced Usage](#advanced-usage) • [FAQ](#faq) •
|
|
32
|
+
[Contributing](#contributing)
|
|
33
|
+
|
|
34
|
+
## Start Here
|
|
35
|
+
|
|
36
|
+
Install with the package manager that matches your runtime:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Deno / JSR
|
|
40
|
+
deno add jsr:@okikio/observables
|
|
41
|
+
|
|
42
|
+
# npm-compatible runtimes
|
|
43
|
+
npm install @okikio/observables
|
|
44
|
+
# pnpm add @okikio/observables
|
|
45
|
+
# yarn add @okikio/observables
|
|
46
|
+
# bun add @okikio/observables
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then build a small pipeline:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { filter, map, Observable, pipe } from "@okikio/observables";
|
|
53
|
+
|
|
54
|
+
const values = pipe(
|
|
55
|
+
Observable.of(1, 2, 3, 4),
|
|
56
|
+
filter((value) => value % 2 === 0),
|
|
57
|
+
map((value) => value * 10),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
values.subscribe((value) => console.log(value));
|
|
61
|
+
// 20
|
|
62
|
+
// 40
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Observables** are a **push‑based stream abstraction** for events, data, and
|
|
66
|
+
long‑running operations. Think of them as a **multi‑value Promise** that keeps
|
|
67
|
+
sending values until you tell it to stop, where a Promise gives you one value
|
|
68
|
+
eventually, an Observable can give you many values over time: mouse clicks,
|
|
69
|
+
search results, chat messages, sensor readings.
|
|
70
|
+
|
|
71
|
+
If you've ever built a web app, you know this all too well: user clicks, API
|
|
72
|
+
responses, WebSocket messages, timers, file uploads, they all arrive at
|
|
73
|
+
different times and need different handling. Before Observables, we'd all end up
|
|
74
|
+
with a mess of callbacks, Promise chains, event listeners, and async/await
|
|
75
|
+
scattered throughout our code.
|
|
76
|
+
|
|
77
|
+
Let's say you're building a search box. You've probably written something like
|
|
78
|
+
this:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// We've all been here: callbacks, timers, and manual cleanup 😫
|
|
82
|
+
let searchTimeout: number;
|
|
83
|
+
let lastRequest: Promise<any> | null = null;
|
|
84
|
+
|
|
85
|
+
searchInput.addEventListener("input", async (event) => {
|
|
86
|
+
const query = event.target.value;
|
|
87
|
+
|
|
88
|
+
// Debounce: wait 300ms after user stops typing
|
|
89
|
+
clearTimeout(searchTimeout);
|
|
90
|
+
searchTimeout = setTimeout(async () => {
|
|
91
|
+
// Cancel previous request somehow?
|
|
92
|
+
if (lastRequest) {
|
|
93
|
+
// How do you cancel a fetch? 🤔
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (query.length < 3) return; // Skip short queries
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
lastRequest = fetch(`/search?q=${query}`);
|
|
100
|
+
const response = await lastRequest;
|
|
101
|
+
const results = await response.json();
|
|
102
|
+
|
|
103
|
+
// Update UI, but what if user already typed something new?
|
|
104
|
+
updateSearchResults(results);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Handle errors, but which errors? Network? Parsing?
|
|
107
|
+
handleSearchError(error);
|
|
108
|
+
}
|
|
109
|
+
}, 300);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Don't forget cleanup when component unmounts!
|
|
113
|
+
// (Spoiler: we all forget this and create memory leaks)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
This works but it's fragile, hard to test, and easy to mess up. Plus, you have
|
|
117
|
+
to remember to clean up event listeners, cancel timers, and handle edge cases
|
|
118
|
+
manually.
|
|
119
|
+
|
|
120
|
+
We've all felt this pain before:
|
|
121
|
+
|
|
122
|
+
- **Memory Leaks**: Forgot to remove an event listener? Your app slowly eats
|
|
123
|
+
memory
|
|
124
|
+
- **Race Conditions**: User clicks fast, requests arrive out of order, wrong
|
|
125
|
+
results appear
|
|
126
|
+
- **Error Handling**: Network failed? Now you need custom backoff and error
|
|
127
|
+
recovery
|
|
128
|
+
- **Backpressure**: Producer too fast for consumer? Memory bloats until crash
|
|
129
|
+
- **Testing**: Complex async flows become nearly impossible to test reliably
|
|
130
|
+
- **Maintenance**: Each async pattern needs its own cleanup and error handling
|
|
131
|
+
|
|
132
|
+
Here's the same search box with Observables:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
// Much cleaner: composable and robust ✨
|
|
136
|
+
import { debounce, filter, map, pipe, switchMap } from "@okikio/observables";
|
|
137
|
+
|
|
138
|
+
const searchResults = pipe(
|
|
139
|
+
inputEvents, // Stream of input events
|
|
140
|
+
debounce(300), // Wait 300ms after user stops typing
|
|
141
|
+
filter((query) => query.length >= 3), // Skip short queries
|
|
142
|
+
switchMap((query) =>
|
|
143
|
+
// Cancel previous requests automatically
|
|
144
|
+
Observable.from(fetch(`/search?q=${query}`))
|
|
145
|
+
),
|
|
146
|
+
map((response) => response.json()), // Parse response
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Subscribe to results (with automatic cleanup!)
|
|
150
|
+
using subscription = searchResults.subscribe({
|
|
151
|
+
next: (results) => updateSearchResults(results),
|
|
152
|
+
error: (error) => handleSearchError(error),
|
|
153
|
+
});
|
|
154
|
+
// Subscription automatically cleaned up when leaving scope
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Notice the difference? No manual timers, no cancellation logic, no memory leaks.
|
|
158
|
+
The operators handle all the complex async coordination for you.
|
|
159
|
+
|
|
160
|
+
This library was built by developers who've felt these same frustrations. It
|
|
161
|
+
focuses on:
|
|
162
|
+
|
|
163
|
+
- **Familiarity**: If you know `Array.map()`, you already understand operators
|
|
164
|
+
- **Performance**: Built on Web Streams with pre-compiled error handling
|
|
165
|
+
- **Type Safety**: Full TypeScript support with intelligent inference
|
|
166
|
+
- **Standards**: Follows the TC39 Observable proposal for future compatibility
|
|
167
|
+
- **Practicality**: <4KB but includes everything you need for real apps
|
|
168
|
+
- **Flexibility**: 4 different error handling modes for different situations
|
|
169
|
+
|
|
170
|
+
## Installation
|
|
171
|
+
|
|
172
|
+
### Deno
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { map, Observable, pipe } from "jsr:@okikio/observables";
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Or
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
deno add jsr:@okikio/observables
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Node.js and Bun
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
npm install @okikio/observables
|
|
188
|
+
# pnpm add @okikio/observables
|
|
189
|
+
# yarn add @okikio/observables
|
|
190
|
+
# bun add @okikio/observables
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
If you prefer to install through the JSR bridge instead of the npm registry:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
npx jsr add @okikio/observables
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
<details>
|
|
200
|
+
<summary>Others</summary>
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
pnpm add jsr:@okikio/observables
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Or
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
yarn add @okikio/observables@jsr:latest
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Or
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
bunx jsr add @okikio/observables
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
</details>
|
|
219
|
+
|
|
220
|
+
### Web
|
|
221
|
+
|
|
222
|
+
You can also use it via a CDN:
|
|
223
|
+
|
|
224
|
+
```ts ignore
|
|
225
|
+
import { map, Observable, pipe } from "https://esm.sh/jsr/@okikio/observables";
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Quick Start
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import { debounce, filter, map, Observable, pipe } from "@okikio/observables";
|
|
232
|
+
|
|
233
|
+
// Create from anything async
|
|
234
|
+
const clicks = new Observable((observer) => {
|
|
235
|
+
const handler = (e) => observer.next(e);
|
|
236
|
+
button.addEventListener("click", handler);
|
|
237
|
+
return () => button.removeEventListener("click", handler);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Transform with operators (like Array.map, but for async data)
|
|
241
|
+
const doubleClicks = pipe(
|
|
242
|
+
clicks,
|
|
243
|
+
debounce(300), // Wait 300ms between clicks
|
|
244
|
+
filter((_, index) => index % 2), // Only odd-numbered clicks
|
|
245
|
+
map((event) => ({ x: event.clientX, y: event.clientY })),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// Subscribe to results
|
|
249
|
+
using subscription = doubleClicks.subscribe({
|
|
250
|
+
next: (coords) => console.log("Double click at:", coords),
|
|
251
|
+
error: (err) => console.error("Error:", err),
|
|
252
|
+
});
|
|
253
|
+
// Automatically cleaned up when leaving scope
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Showcase
|
|
257
|
+
|
|
258
|
+
A couple sites/projects that use `@okikio/observables`:
|
|
259
|
+
|
|
260
|
+
- Your site/project here...
|
|
261
|
+
|
|
262
|
+
## API
|
|
263
|
+
|
|
264
|
+
The API of `@okikio/observables` provides everything you need for reactive
|
|
265
|
+
programming:
|
|
266
|
+
|
|
267
|
+
### Core Observable
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
import { Observable } from "@okikio/observables";
|
|
271
|
+
|
|
272
|
+
// Create observables
|
|
273
|
+
const timer = new Observable((observer) => {
|
|
274
|
+
const id = setInterval(() => observer.next(Date.now()), 1000);
|
|
275
|
+
return () => clearInterval(id);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Factory methods
|
|
279
|
+
Observable.of(1, 2, 3); // From values
|
|
280
|
+
Observable.from(fetch("/api/data")); // From promises/iterables
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Operators (19+ included)
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
import { debounce, filter, map, pipe, switchMap } from "@okikio/observables";
|
|
287
|
+
|
|
288
|
+
// Transform data as it flows
|
|
289
|
+
pipe(
|
|
290
|
+
source,
|
|
291
|
+
map((x) => x * 2), // Transform each value
|
|
292
|
+
filter((x) => x > 10), // Keep only values > 10
|
|
293
|
+
debounce(300), // Wait for quiet periods
|
|
294
|
+
switchMap((x) => fetchData(x)), // Cancel previous requests
|
|
295
|
+
);
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### EventBus & EventDispatcher
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
import { createEventDispatcher, EventBus } from "@okikio/observables";
|
|
302
|
+
|
|
303
|
+
// Simple pub/sub
|
|
304
|
+
const bus = new EventBus<string>();
|
|
305
|
+
bus.events.subscribe((msg) => console.log(msg));
|
|
306
|
+
bus.emit("Hello world!");
|
|
307
|
+
|
|
308
|
+
// Type-safe events
|
|
309
|
+
interface AppEvents {
|
|
310
|
+
userLogin: { userId: string };
|
|
311
|
+
cartUpdate: { items: number };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const events = createEventDispatcher<AppEvents>();
|
|
315
|
+
events.emit("userLogin", { userId: "123" });
|
|
316
|
+
events.on("cartUpdate", (data) => updateUI(data.items));
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Error Handling (4 modes)
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
import { createOperator } from "@okikio/observables";
|
|
323
|
+
|
|
324
|
+
// Choose your error handling strategy
|
|
325
|
+
const processor = createOperator({
|
|
326
|
+
errorMode: "pass-through", // Errors become values (default)
|
|
327
|
+
// errorMode: 'ignore', // Skip errors silently
|
|
328
|
+
// errorMode: 'throw', // Fail fast
|
|
329
|
+
// errorMode: 'manual', // You handle everything
|
|
330
|
+
|
|
331
|
+
transform(value, controller) {
|
|
332
|
+
controller.enqueue(processValue(value));
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Resource Management
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
// Automatic cleanup with 'using'
|
|
341
|
+
{
|
|
342
|
+
using subscription = observable.subscribe(handleData);
|
|
343
|
+
// Use subscription here...
|
|
344
|
+
} // Automatically cleaned up
|
|
345
|
+
|
|
346
|
+
// Async cleanup
|
|
347
|
+
async function example() {
|
|
348
|
+
await using bus = new EventBus();
|
|
349
|
+
// Do async work...
|
|
350
|
+
} // Awaits cleanup
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Pull API (Async Iteration)
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
// Process large datasets with backpressure
|
|
357
|
+
for await (
|
|
358
|
+
const chunk of bigDataStream.pull({
|
|
359
|
+
strategy: { highWaterMark: 8 }, // Small buffer for large files
|
|
360
|
+
})
|
|
361
|
+
) {
|
|
362
|
+
await processChunk(chunk);
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Look through the [tests/](./tests/) and [bench/](./bench/) folders for complex
|
|
367
|
+
examples and multiple usage patterns.
|
|
368
|
+
|
|
369
|
+
## Advanced Usage
|
|
370
|
+
|
|
371
|
+
### Smart Search with Cancellation
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
import {
|
|
375
|
+
catchErrors,
|
|
376
|
+
debounce,
|
|
377
|
+
filter,
|
|
378
|
+
map,
|
|
379
|
+
pipe,
|
|
380
|
+
switchMap,
|
|
381
|
+
} from "@okikio/observables";
|
|
382
|
+
|
|
383
|
+
const searchResults = pipe(
|
|
384
|
+
searchInput,
|
|
385
|
+
debounce(300), // Wait for typing pause
|
|
386
|
+
filter((query) => query.length > 2), // Skip short queries
|
|
387
|
+
switchMap((query) =>
|
|
388
|
+
// Cancel old requests automatically
|
|
389
|
+
pipe(
|
|
390
|
+
Observable.from(fetch(`/search?q=${query}`)),
|
|
391
|
+
map((res) => res.json()),
|
|
392
|
+
catchErrors([]), // Return empty array on error
|
|
393
|
+
)
|
|
394
|
+
),
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
searchResults.subscribe((results) => updateUI(results));
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Real-Time Dashboard
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
import { filter, pipe, scan, throttle } from "@okikio/observables";
|
|
404
|
+
|
|
405
|
+
const dashboardData = pipe(
|
|
406
|
+
webSocketEvents,
|
|
407
|
+
filter((event) => event.type === "metric"), // Only metric events
|
|
408
|
+
scan((acc, event) => ({ // Build running totals
|
|
409
|
+
total: acc.total + event.value,
|
|
410
|
+
count: acc.count + 1,
|
|
411
|
+
average: (acc.total + event.value) / (acc.count + 1),
|
|
412
|
+
}), { total: 0, count: 0, average: 0 }),
|
|
413
|
+
throttle(1000), // Update UI max once per second
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
dashboardData.subscribe((stats) => updateDashboard(stats));
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Custom Operators
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
import { createOperator, createStatefulOperator } from "@okikio/observables";
|
|
423
|
+
|
|
424
|
+
// Simple transformation
|
|
425
|
+
function double<T extends number>() {
|
|
426
|
+
return createOperator<T, T>({
|
|
427
|
+
name: "double",
|
|
428
|
+
transform(value, controller) {
|
|
429
|
+
controller.enqueue(value * 2);
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Stateful operation
|
|
435
|
+
function movingAverage(windowSize: number) {
|
|
436
|
+
return createStatefulOperator<number, number, number[]>({
|
|
437
|
+
name: "movingAverage",
|
|
438
|
+
createState: () => [],
|
|
439
|
+
|
|
440
|
+
transform(value, arr, controller) {
|
|
441
|
+
arr.push(value);
|
|
442
|
+
if (arr.length > windowSize) arr.shift();
|
|
443
|
+
|
|
444
|
+
const avg = arr.reduce((sum, n) => sum + n, 0) / arr.length;
|
|
445
|
+
controller.enqueue(avg);
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## Performance
|
|
452
|
+
|
|
453
|
+
We built this on Web Streams for good reason, native backpressure and memory
|
|
454
|
+
efficiency come for free. Here's what you get:
|
|
455
|
+
|
|
456
|
+
- **Web Streams Foundation**: Handles backpressure automatically, no memory
|
|
457
|
+
bloat
|
|
458
|
+
- **Pre-compiled Error Modes**: Skip runtime checks in hot paths
|
|
459
|
+
- **Tree Shaking**: Import only what you use (most apps need <4KB)
|
|
460
|
+
- **TypeScript Native**: Zero runtime overhead for type safety
|
|
461
|
+
|
|
462
|
+
Performance varies by use case, but here's how different error modes stack up:
|
|
463
|
+
|
|
464
|
+
| Error Mode | Performance | When We Use It |
|
|
465
|
+
| -------------- | ----------- | ------------------------- |
|
|
466
|
+
| `manual` | Fastest | Hot paths, custom logic |
|
|
467
|
+
| `ignore` | Very fast | Filtering bad data |
|
|
468
|
+
| `pass-through` | Fast | Error recovery, debugging |
|
|
469
|
+
| `throw` | Good | Fail-fast validation |
|
|
470
|
+
|
|
471
|
+
## Comparison
|
|
472
|
+
|
|
473
|
+
| Feature | @okikio/observables | RxJS | zen-observable |
|
|
474
|
+
| --------------- | ------------------- | ----------- | -------------- |
|
|
475
|
+
| Bundle Size | <4KB | ~35KB | ~2KB |
|
|
476
|
+
| Operators | 19+ | 100+ | 5 |
|
|
477
|
+
| Error Modes | 4 modes | 1 mode | 1 mode |
|
|
478
|
+
| EventBus | ✅ Built-in | ❌ Separate | ❌ None |
|
|
479
|
+
| TC39 Compliance | ✅ Yes | ⚠️ Partial | ✅ Yes |
|
|
480
|
+
| TypeScript | ✅ Native | ✅ Yes | ⚠️ Basic |
|
|
481
|
+
| Tree Shaking | ✅ Perfect | ⚠️ Partial | ✅ Yes |
|
|
482
|
+
| Learning Curve | 🟢 Gentle | 🔴 Steep | 🟢 Gentle |
|
|
483
|
+
|
|
484
|
+
## Browser Support
|
|
485
|
+
|
|
486
|
+
| Chrome | Edge | Firefox | Safari | Node | Deno | Bun |
|
|
487
|
+
| ------ | ---- | ------- | ------ | ---- | ---- | ---- |
|
|
488
|
+
| 80+ | 80+ | 72+ | 13+ | 16+ | 1.0+ | 1.0+ |
|
|
489
|
+
|
|
490
|
+
> Native support for Observables is excellent. Some advanced features like
|
|
491
|
+
> `Symbol.dispose` require newer environments or polyfills.
|
|
492
|
+
|
|
493
|
+
## FAQ
|
|
494
|
+
|
|
495
|
+
### What are Observables exactly?
|
|
496
|
+
|
|
497
|
+
Think of them as Promises that can send multiple values over time. Where a
|
|
498
|
+
Promise gives you one result eventually, an Observable can keep sending values,
|
|
499
|
+
like a stream of search results, mouse movements, or WebSocket messages.
|
|
500
|
+
|
|
501
|
+
### Why not just use RxJS?
|
|
502
|
+
|
|
503
|
+
RxJS is powerful but can be overwhelming. We've all been there, 100+ operators,
|
|
504
|
+
steep learning curve, 35KB bundle size. This library gives you the essential
|
|
505
|
+
Observable patterns you actually use day-to-day, following the TC39 proposal so
|
|
506
|
+
you're future-ready.
|
|
507
|
+
|
|
508
|
+
### EventBus vs Observable, when do I use which?
|
|
509
|
+
|
|
510
|
+
Good question! Here's how we think about it:
|
|
511
|
+
|
|
512
|
+
- **Observable**: When you're transforming data one-to-one (API calls,
|
|
513
|
+
processing user input)
|
|
514
|
+
- **EventBus**: When you need one-to-many communication (notifications,
|
|
515
|
+
cross-component events)
|
|
516
|
+
|
|
517
|
+
### How should I handle errors?
|
|
518
|
+
|
|
519
|
+
Pick the mode that fits your situation:
|
|
520
|
+
|
|
521
|
+
- **`pass-through`**: Errors become values you can recover from
|
|
522
|
+
- **`ignore`**: Skip errors silently (great for filtering noisy data)
|
|
523
|
+
- **`throw`**: Fail fast for validation
|
|
524
|
+
- **`manual`**: Handle everything yourself
|
|
525
|
+
|
|
526
|
+
### Is this actually production ready?
|
|
527
|
+
|
|
528
|
+
We use it in production. It follows the TC39 proposal, has comprehensive tests,
|
|
529
|
+
and handles resource management properly. The Web Streams foundation is
|
|
530
|
+
battle-tested across browsers and runtimes.
|
|
531
|
+
|
|
532
|
+
## Contributing
|
|
533
|
+
|
|
534
|
+
Contributions are welcome. This project targets Deno v2+ and keeps a tight
|
|
535
|
+
feedback loop around formatting, linting, docs, tests, and npm packaging, so a
|
|
536
|
+
good contribution usually starts by getting the local validation commands
|
|
537
|
+
working first.
|
|
538
|
+
|
|
539
|
+
Install Deno with [mise](https://mise.jdx.dev/) or by following the
|
|
540
|
+
[manual installation guide](https://deno.land/manual/getting_started/installation).
|
|
541
|
+
|
|
542
|
+
### Setup with Mise
|
|
543
|
+
|
|
544
|
+
```bash
|
|
545
|
+
curl https://mise.run | sh
|
|
546
|
+
echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Then install the toolchain:
|
|
550
|
+
|
|
551
|
+
```bash
|
|
552
|
+
mise install
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Validate your change
|
|
556
|
+
|
|
557
|
+
```bash
|
|
558
|
+
deno fmt
|
|
559
|
+
deno lint
|
|
560
|
+
deno check **/*.ts
|
|
561
|
+
deno doc --lint mod.ts
|
|
562
|
+
deno task test
|
|
563
|
+
deno task build:npm
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
If your change is performance-sensitive, also run:
|
|
567
|
+
|
|
568
|
+
```bash
|
|
569
|
+
deno task bench
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
This repository uses
|
|
573
|
+
[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/), so
|
|
574
|
+
please format commit messages accordingly.
|
|
575
|
+
|
|
576
|
+
## License
|
|
577
|
+
|
|
578
|
+
See the [LICENSE](./LICENSE) file for license rights and limitations (MIT).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
interface Error {
|
|
3
|
+
cause?: unknown;
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
export {};
|
|
7
|
+
declare global {
|
|
8
|
+
interface PromiseConstructor {
|
|
9
|
+
/**
|
|
10
|
+
* Creates a Promise that can be resolved or rejected using provided functions.
|
|
11
|
+
* @returns An object containing `promise` promise object, `resolve` and `reject` functions.
|
|
12
|
+
*/
|
|
13
|
+
withResolvers<T>(): {
|
|
14
|
+
promise: Promise<T>;
|
|
15
|
+
resolve: (value: T | PromiseLike<T>) => void;
|
|
16
|
+
reject: (reason?: any) => void;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=_dnt.polyfills.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_dnt.polyfills.d.ts","sourceRoot":"","sources":["../src/_dnt.polyfills.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,KAAK;QACb,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;CACF;AAED,OAAO,EAAE,CAAC;AACV,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,kBAAkB;QAC1B;;;WAGG;QACH,aAAa,CAAC,CAAC,KAAK;YAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;YAAC,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;YAAC,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;SAAE,CAAC;KAC3H;CACF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// https://github.com/tc39/proposal-promise-with-resolvers/blob/3a78801e073e99217dbeb2c43ba7212f3bdc8b83/polyfills.js#L1C1-L9C2
|
|
2
|
+
if (Promise.withResolvers === undefined) {
|
|
3
|
+
Promise.withResolvers = () => {
|
|
4
|
+
const out = {};
|
|
5
|
+
out.promise = new Promise((resolve_, reject_) => {
|
|
6
|
+
out.resolve = resolve_;
|
|
7
|
+
out.reject = reject_;
|
|
8
|
+
});
|
|
9
|
+
return out;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export {};
|