@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
|
@@ -0,0 +1,1984 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
5
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
6
|
+
};
|
|
7
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
8
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
9
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
10
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
11
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
12
|
+
};
|
|
13
|
+
var _SubscriptionObserver_state, _SubscriptionObserver_subscription, _Observable_subscribeFn;
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.Observable = exports.SubscriptionObserver = exports.SubscriptionStateMap = void 0;
|
|
16
|
+
exports.createSubscription = createSubscription;
|
|
17
|
+
exports.markSubscriptionClosed = markSubscriptionClosed;
|
|
18
|
+
exports.performSubscriptionCleanup = performSubscriptionCleanup;
|
|
19
|
+
exports.closeSubscription = closeSubscription;
|
|
20
|
+
exports.of = of;
|
|
21
|
+
exports.from = from;
|
|
22
|
+
exports.pull = pull;
|
|
23
|
+
exports.isObservable = isObservable;
|
|
24
|
+
exports.isSpecObservable = isSpecObservable;
|
|
25
|
+
// @filename: observable.ts
|
|
26
|
+
/**
|
|
27
|
+
* A **spec-faithful** yet ergonomic TC39-inspired Observable implementation with detailed TSDocs and examples.
|
|
28
|
+
*
|
|
29
|
+
* A **push‑based stream abstraction** for events, data, and long‑running
|
|
30
|
+
* operations. Think of it as a **multi‑value Promise** that keeps sending
|
|
31
|
+
* values until you tell it to stop.
|
|
32
|
+
*
|
|
33
|
+
* ## Why This Exists
|
|
34
|
+
* Apps juggle many async sources, mouse clicks, HTTP requests, timers,
|
|
35
|
+
* WebSockets, file watchers. Before Observables you glued those together with a
|
|
36
|
+
* mish‑mash of callbacks, Promises, `EventTarget`s and async iterators, each
|
|
37
|
+
* with different rules for cleanup and error handling. **Observables give you
|
|
38
|
+
* one mental model** for subscription → cancellation → propagation → teardown.
|
|
39
|
+
*
|
|
40
|
+
* ## ✨ Feature Highlights
|
|
41
|
+
* - **Unified push + pull** – use callbacks *or* `for await … of` on the same
|
|
42
|
+
* stream.
|
|
43
|
+
* - **Cold by default** – each subscriber gets an independent execution (great
|
|
44
|
+
* for predictable side‑effects).
|
|
45
|
+
* - **Deterministic teardown** – return a function/`unsubscribe`/`[Symbol.dispose]`
|
|
46
|
+
* and it *always* runs once, even if the observable errors synchronously.
|
|
47
|
+
* - **Back‑pressure helper** – `pull()` converts to an `AsyncGenerator` backed
|
|
48
|
+
* by `ReadableStream` so the producer slows down when the consumer lags.
|
|
49
|
+
* - **Tiny surface** – <1 kB min+gzip of logic; treeshakes cleanly.
|
|
50
|
+
*
|
|
51
|
+
* ## Error Propagation Policy
|
|
52
|
+
* 1. **Local catch** – If your observer supplies an `error` callback, **all**
|
|
53
|
+
* upstream errors funnel there.
|
|
54
|
+
* 2. **Unhandled‑rejection style** – If no `error` handler is provided the
|
|
55
|
+
* exception is re‑thrown on the micro‑task queue (same timing semantics as
|
|
56
|
+
* an unhandled Promise rejection).
|
|
57
|
+
* 3. **Observer callback failures** – Exceptions thrown inside `next()` or
|
|
58
|
+
* `complete()` are routed to `error()` if present, otherwise bubble as in
|
|
59
|
+
* (2).
|
|
60
|
+
* 4. **Errors inside `error()`** – A second‑level failure is *always* queued to
|
|
61
|
+
* the micro‑task queue to avoid infinite recursion.
|
|
62
|
+
*
|
|
63
|
+
* ## Edge‑Cases & Gotchas
|
|
64
|
+
* - `subscribe()` can synchronously call `complete()`/`error()` and still have
|
|
65
|
+
* its teardown captured – **ordering is guaranteed**.
|
|
66
|
+
* - Subscribing twice to a *cold* observable triggers two side‑effects (e.g.
|
|
67
|
+
* two HTTP requests). Share the source if you want fan‑out.
|
|
68
|
+
* - Infinite streams leak unless you call `unsubscribe()` or wrap them in a
|
|
69
|
+
* `using` block.
|
|
70
|
+
* - The helper `pull()` encodes thrown errors as `ObservableError` *values* so
|
|
71
|
+
* buffered items are not lost – remember to `instanceof` check if you rely
|
|
72
|
+
* on it.
|
|
73
|
+
*
|
|
74
|
+
* @example Common Patterns
|
|
75
|
+
* ```ts
|
|
76
|
+
* // DOM events → Observable
|
|
77
|
+
* const clicks = new Observable<Event>(obs => {
|
|
78
|
+
* const h = (e: Event) => obs.next(e);
|
|
79
|
+
* button.addEventListener("click", h);
|
|
80
|
+
* return () => button.removeEventListener("click", h);
|
|
81
|
+
* });
|
|
82
|
+
*
|
|
83
|
+
* // HTTP polling every 5 s
|
|
84
|
+
* const poll = new Observable<Response>(obs => {
|
|
85
|
+
* const id = setInterval(async () => {
|
|
86
|
+
* try { obs.next(await fetch("/api/data")); }
|
|
87
|
+
* catch (e) { obs.error(e); }
|
|
88
|
+
* }, 5000);
|
|
89
|
+
* return () => clearInterval(id);
|
|
90
|
+
* });
|
|
91
|
+
*
|
|
92
|
+
* // WebSocket stream with graceful close
|
|
93
|
+
* const live = new Observable<string>(obs => {
|
|
94
|
+
* const ws = new WebSocket("wss://example.com");
|
|
95
|
+
* ws.onmessage = e => obs.next(e.data);
|
|
96
|
+
* ws.onerror = e => obs.error(e);
|
|
97
|
+
* ws.onclose = () => obs.complete();
|
|
98
|
+
* return () => ws.close();
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* @example Basic subscription:
|
|
103
|
+
* ```ts
|
|
104
|
+
* import { Observable } from './observable.ts';
|
|
105
|
+
*
|
|
106
|
+
* // Emit 1,2,3 then complete
|
|
107
|
+
* const subscription = Observable.of(1, 2, 3).subscribe({
|
|
108
|
+
* start(sub) { console.log('Subscribed'); },
|
|
109
|
+
* next(val) { console.log('Value:', val); },
|
|
110
|
+
* complete() { console.log('Complete'); }
|
|
111
|
+
* });
|
|
112
|
+
*
|
|
113
|
+
* // Cancel manually if needed
|
|
114
|
+
* subscription.unsubscribe();
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* @example Resource-safe usage with `using` statement:
|
|
118
|
+
* ```ts
|
|
119
|
+
* import { Observable } from './observable.ts';
|
|
120
|
+
*
|
|
121
|
+
* {
|
|
122
|
+
* using subscription = Observable.of(1, 2, 3).subscribe({
|
|
123
|
+
* next(val) { console.log('Value:', val); }
|
|
124
|
+
* });
|
|
125
|
+
*
|
|
126
|
+
* // Code that uses the subscription
|
|
127
|
+
* doSomething();
|
|
128
|
+
*
|
|
129
|
+
* } // Subscription automatically unsubscribed at block end
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* @example Simple async iteration:
|
|
133
|
+
* ```ts
|
|
134
|
+
* import { Observable } from './observable.ts';
|
|
135
|
+
*
|
|
136
|
+
* (async () => {
|
|
137
|
+
* for await (const x of Observable.of('a', 'b', 'c')) {
|
|
138
|
+
* console.log(x);
|
|
139
|
+
* }
|
|
140
|
+
* })();
|
|
141
|
+
* ```
|
|
142
|
+
*
|
|
143
|
+
* @example Pull with backpressure:
|
|
144
|
+
* ```ts
|
|
145
|
+
* import { Observable } from './observable.ts';
|
|
146
|
+
*
|
|
147
|
+
* const nums = Observable.from([1,2,3,4,5]);
|
|
148
|
+
* (async () => {
|
|
149
|
+
* for await (const n of nums.pull({ strategy: { highWaterMark: 2 } })) {
|
|
150
|
+
* console.log('Pulled:', n);
|
|
151
|
+
* await new Promise(r => setTimeout(r, 1000)); // Slow consumer
|
|
152
|
+
* }
|
|
153
|
+
* })();
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* ## Spec Compliance & Notable Deviations
|
|
157
|
+
* | Area | Proposal Behaviour | This Library |
|
|
158
|
+
* |----------------------------|----------------------------------------|-----------------------------------------------------------------------------------------|
|
|
159
|
+
* | `subscribe` parameters | Only **observer object** | Adds `(next, error?, complete?)` triple‑param overload. |
|
|
160
|
+
* | Teardown shape | Function or `{ unsubscribe() }` | Also honours `[Symbol.dispose]` **and** `[Symbol.asyncDispose]`. |
|
|
161
|
+
* | Pull‑mode iteration | *Not in spec* | `pull()` helper returns an `AsyncGenerator` with `ReadableStream`‑backed back‑pressure. |
|
|
162
|
+
* | Error propagation in pull | Stream **error** ends iteration | Error encoded as `ObservableError` value so buffered items drain first. |
|
|
163
|
+
* | `Symbol.toStringTag` | Optional | Provided for `Observable` and `SubscriptionObserver`. |
|
|
164
|
+
*
|
|
165
|
+
* Anything not listed above matches the TC39 draft (**May 2025**).
|
|
166
|
+
*
|
|
167
|
+
* ## Lifecycle State Machine
|
|
168
|
+
* ```text
|
|
169
|
+
* (inactive) --subscribe()--> [ active ]
|
|
170
|
+
* ^ | next()
|
|
171
|
+
* | unsubscribe()/error() | complete()
|
|
172
|
+
* |<------------------------| (closed)
|
|
173
|
+
* ```
|
|
174
|
+
* *Teardown executes exactly once on the leftward arrow.*
|
|
175
|
+
*
|
|
176
|
+
* @example Type‑Parameter Primer
|
|
177
|
+
* ```ts
|
|
178
|
+
* Observable<number> // counter
|
|
179
|
+
* Observable<Response> // fetch responses
|
|
180
|
+
* Observable<{x:number;y:number}> // mouse coords
|
|
181
|
+
* Observable<never> // signal‑only (no payload)
|
|
182
|
+
* Observable<string | ErrorPayload> // unions are fine
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
185
|
+
* @example Interop Cheat‑Sheet
|
|
186
|
+
* ```ts
|
|
187
|
+
* // Promise → Observable (single value then complete)
|
|
188
|
+
* Observable.from(fetch("/api"));
|
|
189
|
+
*
|
|
190
|
+
* // Observable → async iterator (back‑pressure aware)
|
|
191
|
+
* for await (const chunk of obs) {
|
|
192
|
+
* processChunk(chunk);
|
|
193
|
+
* }
|
|
194
|
+
*
|
|
195
|
+
* // Observable → Promise (first value only)
|
|
196
|
+
* const first = (await obs.pull().next()).value;
|
|
197
|
+
* ```
|
|
198
|
+
*
|
|
199
|
+
* ## Performance Cookbook (pull())
|
|
200
|
+
* | Producer speed | Consumer speed | Suggested `highWaterMark` | Notes |
|
|
201
|
+
* |---------------:|---------------:|--------------------------:|-----------------------------------------|
|
|
202
|
+
* | 🔥 Very fast | 🐢 Slow | 1‑8 | Minimal RAM; heavy throttling. |
|
|
203
|
+
* | ⚡ Fast | 🚶 Moderate | 16‑64 (default 64) | Good balance for most apps. |
|
|
204
|
+
* | 🚀 Bursty | 🚀 Bursty | 128‑512 | Smooths spikes at the cost of memory. |
|
|
205
|
+
*
|
|
206
|
+
* ➜ If RSS climbs steadily, halve `highWaterMark`; if you’re dropping messages
|
|
207
|
+
* under load, raise it (RAM permitting).
|
|
208
|
+
*
|
|
209
|
+
* ## Memory Management
|
|
210
|
+
*
|
|
211
|
+
* **Critical**: Infinite Observables need manual cleanup via `unsubscribe()` or `using` blocks
|
|
212
|
+
* to prevent memory leaks. Finite Observables auto-cleanup on complete/error.
|
|
213
|
+
*
|
|
214
|
+
* @example Quick start - DOM events
|
|
215
|
+
* ```ts
|
|
216
|
+
* const clicks = new Observable(observer => {
|
|
217
|
+
* const handler = e => observer.next(e);
|
|
218
|
+
* button.addEventListener('click', handler);
|
|
219
|
+
* return () => button.removeEventListener('click', handler);
|
|
220
|
+
* });
|
|
221
|
+
*
|
|
222
|
+
* using subscription = clicks.subscribe(event => console.log('Clicked!'));
|
|
223
|
+
* // Auto-cleanup when leaving scope
|
|
224
|
+
* ```
|
|
225
|
+
*
|
|
226
|
+
* @example Network with backpressure
|
|
227
|
+
* ```ts
|
|
228
|
+
* const dataStream = new Observable(observer => {
|
|
229
|
+
* const ws = new WebSocket('ws://api.com/live');
|
|
230
|
+
* ws.onmessage = e => observer.next(JSON.parse(e.data));
|
|
231
|
+
* ws.onerror = e => observer.error(e);
|
|
232
|
+
* return () => ws.close();
|
|
233
|
+
* });
|
|
234
|
+
*
|
|
235
|
+
* // Consume at controlled pace
|
|
236
|
+
* for await (const data of dataStream.pull({ strategy: { highWaterMark: 10 } })) {
|
|
237
|
+
* await processSlowly(data); // Producer pauses when buffer fills
|
|
238
|
+
* }
|
|
239
|
+
* ```
|
|
240
|
+
*
|
|
241
|
+
* @example Testing & Debugging Tips
|
|
242
|
+
* ```ts
|
|
243
|
+
* import { expect, test } from "jsr:@libs/testing@^5";
|
|
244
|
+
*
|
|
245
|
+
* test("emits three ticks then completes", async () => {
|
|
246
|
+
* const ticks = Observable.of(1, 2, 3);
|
|
247
|
+
* const out: number[] = [];
|
|
248
|
+
* for await (const n of ticks) out.push(n);
|
|
249
|
+
* expect(out).toEqual([1, 2, 3]);
|
|
250
|
+
* });
|
|
251
|
+
*
|
|
252
|
+
* // Quick console probe
|
|
253
|
+
* obs.subscribe(v => console.log("[OBS]", v));
|
|
254
|
+
* ```
|
|
255
|
+
*
|
|
256
|
+
* ## FAQ
|
|
257
|
+
* - **Why does my network request fire twice?** Cold observables run once per
|
|
258
|
+
* subscribe. Reuse a single subscription or share the source.
|
|
259
|
+
* - **Why does `next()` throw after `complete()`?** The stream is closed; calls
|
|
260
|
+
* are ignored by design.
|
|
261
|
+
* - **Memory leak on interval** , Infinite streams require `unsubscribe()` or
|
|
262
|
+
* `using`.
|
|
263
|
+
*
|
|
264
|
+
* @module
|
|
265
|
+
*/
|
|
266
|
+
require("./_dnt.polyfills.js");
|
|
267
|
+
const error_js_1 = require("./error.js");
|
|
268
|
+
const symbol_js_1 = require("./symbol.js");
|
|
269
|
+
/**
|
|
270
|
+
* Central registry of subscription state.
|
|
271
|
+
*
|
|
272
|
+
* Using a WeakMap allows us to:
|
|
273
|
+
* 1. Associate state with subscription objects without extending them
|
|
274
|
+
* 2. Let the garbage collector automatically clean up entries when subscriptions are no longer referenced
|
|
275
|
+
* 3. Hide implementation details from users
|
|
276
|
+
*/
|
|
277
|
+
exports.SubscriptionStateMap = new WeakMap();
|
|
278
|
+
/**
|
|
279
|
+
* Creates a new Subscription object with properly initialized state.
|
|
280
|
+
*
|
|
281
|
+
* We validate observer methods early, ensuring type errors are caught
|
|
282
|
+
* at subscription time rather than during event emission.
|
|
283
|
+
*
|
|
284
|
+
* The returned Subscription includes support for:
|
|
285
|
+
* - Manual cancellation via `unsubscribe()`
|
|
286
|
+
* - Automatic cleanup via `using` blocks (Symbol.dispose)
|
|
287
|
+
* - Async cleanup contexts (Symbol.asyncDispose)
|
|
288
|
+
*
|
|
289
|
+
* @throws TypeError if observer methods are present but not functions
|
|
290
|
+
* @internal
|
|
291
|
+
*/
|
|
292
|
+
function createSubscription(observer, opts) {
|
|
293
|
+
// Observer's methods should be functions if they exist
|
|
294
|
+
if (observer.next !== undefined && typeof observer.next !== "function") {
|
|
295
|
+
throw new TypeError("Observer.next must be a function");
|
|
296
|
+
}
|
|
297
|
+
if (observer.error !== undefined && typeof observer.error !== "function") {
|
|
298
|
+
throw new TypeError("Observer.error must be a function");
|
|
299
|
+
}
|
|
300
|
+
if (observer.complete !== undefined && typeof observer.complete !== "function") {
|
|
301
|
+
throw new TypeError("Observer.complete must be a function");
|
|
302
|
+
}
|
|
303
|
+
// Create a local statemap to speed up access during hot-paths
|
|
304
|
+
const stateMap = {
|
|
305
|
+
closed: false,
|
|
306
|
+
observer,
|
|
307
|
+
cleanup: null,
|
|
308
|
+
removeAbortHandler: null,
|
|
309
|
+
};
|
|
310
|
+
/* -------------------------------------------------------------------
|
|
311
|
+
* Create the Subscription facade (spec: CreateSubscription()).
|
|
312
|
+
* ------------------------------------------------------------------- */
|
|
313
|
+
const subscription = {
|
|
314
|
+
get [symbol_js_1.Symbol.toStringTag]() {
|
|
315
|
+
return "Subscription";
|
|
316
|
+
},
|
|
317
|
+
/**
|
|
318
|
+
* Returns whether this subscription is closed.
|
|
319
|
+
*
|
|
320
|
+
* A subscription becomes closed after:
|
|
321
|
+
* - Explicit call to unsubscribe()
|
|
322
|
+
* - Error notification
|
|
323
|
+
* - Complete notification
|
|
324
|
+
*
|
|
325
|
+
* Once closed, no further events will be delivered to the observer,
|
|
326
|
+
* and resources associated with the subscription are released.
|
|
327
|
+
*/
|
|
328
|
+
get closed() {
|
|
329
|
+
return stateMap.closed;
|
|
330
|
+
},
|
|
331
|
+
/**
|
|
332
|
+
* Cancels the subscription and releases resources.
|
|
333
|
+
*
|
|
334
|
+
* - Safe to call multiple times (idempotent)
|
|
335
|
+
* - Synchronously performs cleanup
|
|
336
|
+
* - Marks subscription as closed
|
|
337
|
+
* - Prevents further observer notifications
|
|
338
|
+
*
|
|
339
|
+
* This is the primary method for consumers to explicitly
|
|
340
|
+
* terminate a subscription when they no longer need it.
|
|
341
|
+
*/
|
|
342
|
+
unsubscribe() {
|
|
343
|
+
closeSubscription(this, stateMap);
|
|
344
|
+
},
|
|
345
|
+
// Support `using` disposal for automatic resource management
|
|
346
|
+
[symbol_js_1.Symbol.dispose]() {
|
|
347
|
+
this.unsubscribe();
|
|
348
|
+
},
|
|
349
|
+
// Support async disposal patterns
|
|
350
|
+
[symbol_js_1.Symbol.asyncDispose]() {
|
|
351
|
+
return Promise.resolve(this.unsubscribe());
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
// Adds support for unsubscribing via AbortSignals
|
|
355
|
+
const abortHandler = () => subscription?.unsubscribe();
|
|
356
|
+
const removeAbortHandler = () => opts?.signal?.removeEventListener("abort", abortHandler);
|
|
357
|
+
opts?.signal?.addEventListener?.("abort", abortHandler, { once: true });
|
|
358
|
+
stateMap.removeAbortHandler = removeAbortHandler;
|
|
359
|
+
// Initialize shared state
|
|
360
|
+
exports.SubscriptionStateMap.set(subscription, stateMap);
|
|
361
|
+
return subscription;
|
|
362
|
+
}
|
|
363
|
+
function markSubscriptionClosed(state, returnObserver = false) {
|
|
364
|
+
if (!state || state.closed)
|
|
365
|
+
return null;
|
|
366
|
+
// Capture observer BEFORE marking closed (for spec compliance)
|
|
367
|
+
const observer = state.observer;
|
|
368
|
+
// Mark as closed (this is what SubscriptionClosed checks)
|
|
369
|
+
state.closed = true;
|
|
370
|
+
state.observer = null;
|
|
371
|
+
// Return observer if requested (enables spec-compliant error/complete)
|
|
372
|
+
if (returnObserver)
|
|
373
|
+
return observer;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Perform cleanup if available. Safe to call multiple times.
|
|
377
|
+
*/
|
|
378
|
+
function performSubscriptionCleanup(subscription, state) {
|
|
379
|
+
if (!state)
|
|
380
|
+
return;
|
|
381
|
+
// Cache cleanup, abort signal and the abort handler before clearing
|
|
382
|
+
let cleanup = state.cleanup;
|
|
383
|
+
let removeAbortHandler = state.removeAbortHandler;
|
|
384
|
+
// Only clean if we have something to clean
|
|
385
|
+
if (!cleanup && !removeAbortHandler)
|
|
386
|
+
return;
|
|
387
|
+
// Clear references first
|
|
388
|
+
state.cleanup = null;
|
|
389
|
+
state.removeAbortHandler = null;
|
|
390
|
+
// Remove the abort handler
|
|
391
|
+
removeAbortHandler?.();
|
|
392
|
+
// Run teardown (existing logic preserved)
|
|
393
|
+
try {
|
|
394
|
+
cleanupSubscription(cleanup);
|
|
395
|
+
}
|
|
396
|
+
finally {
|
|
397
|
+
exports.SubscriptionStateMap.delete(subscription);
|
|
398
|
+
cleanup = null;
|
|
399
|
+
removeAbortHandler = null;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Marks a subscription as closed and schedules necessary cleanup.
|
|
404
|
+
*
|
|
405
|
+
* This is the centralized implementation for all subscription termination paths:
|
|
406
|
+
* - Manual unsubscribe()
|
|
407
|
+
* - Observer.error()
|
|
408
|
+
* - Observer.complete()
|
|
409
|
+
*
|
|
410
|
+
* The function ensures:
|
|
411
|
+
* 1. Idempotency (safe to call multiple times)
|
|
412
|
+
* 2. Cleanup happens exactly once
|
|
413
|
+
* 3. State is properly cleared to prevent memory leaks
|
|
414
|
+
* 4. WeakMap entry is removed to aid garbage collection
|
|
415
|
+
*
|
|
416
|
+
* @param subscription - The subscription to close
|
|
417
|
+
* @internal
|
|
418
|
+
*/
|
|
419
|
+
function closeSubscription(subscription, stateMap) {
|
|
420
|
+
const state = stateMap ?? exports.SubscriptionStateMap.get(subscription);
|
|
421
|
+
const closed = markSubscriptionClosed(state);
|
|
422
|
+
if (closed === null)
|
|
423
|
+
return;
|
|
424
|
+
// If we have an observer, perform cleanup
|
|
425
|
+
performSubscriptionCleanup(subscription, state);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Handles the actual cleanup process for a subscription.
|
|
429
|
+
*
|
|
430
|
+
* The spec allows three different types of cleanup values:
|
|
431
|
+
* 1. Function: Called directly
|
|
432
|
+
* 2. Object with unsubscribe method: unsubscribe() is called
|
|
433
|
+
* 3. (deviate from spec) Object with Symbol.dispose/asyncDispose: dispose() is called
|
|
434
|
+
*
|
|
435
|
+
* Any errors during cleanup are reported asynchronously to prevent
|
|
436
|
+
* them from disrupting the unsubscribe flow.
|
|
437
|
+
*
|
|
438
|
+
* @param cleanup - Function or object to perform cleanup
|
|
439
|
+
* @internal
|
|
440
|
+
*/
|
|
441
|
+
function cleanupSubscription(cleanup) {
|
|
442
|
+
let temp = cleanup;
|
|
443
|
+
cleanup = null;
|
|
444
|
+
if (!temp)
|
|
445
|
+
return;
|
|
446
|
+
try {
|
|
447
|
+
if (typeof temp === "function")
|
|
448
|
+
temp();
|
|
449
|
+
else if (typeof temp === "object") {
|
|
450
|
+
if (typeof temp.unsubscribe === "function") {
|
|
451
|
+
temp.unsubscribe();
|
|
452
|
+
}
|
|
453
|
+
else if (typeof temp[symbol_js_1.Symbol.asyncDispose] === "function") {
|
|
454
|
+
temp[symbol_js_1.Symbol.asyncDispose]();
|
|
455
|
+
}
|
|
456
|
+
else if (typeof temp[symbol_js_1.Symbol.dispose] === "function") {
|
|
457
|
+
temp[symbol_js_1.Symbol.dispose]();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
// Report cleanup errors asynchronously to avoid disrupting the unsubscribe flow
|
|
463
|
+
queueMicrotask(() => {
|
|
464
|
+
throw err;
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
temp = null;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Wraps an observer with key guarantees required by the Observable specification.
|
|
471
|
+
*
|
|
472
|
+
* SubscriptionObserver is a critical component that ensures:
|
|
473
|
+
*
|
|
474
|
+
* 1. The observer contract is honored correctly
|
|
475
|
+
* 2. Notifications stop after a subscription is closed
|
|
476
|
+
* 3. Error/complete notifications properly terminate the subscription
|
|
477
|
+
* 4. Observer methods are called with the correct `this` context
|
|
478
|
+
* 5. Errors are properly propagated according to spec
|
|
479
|
+
*
|
|
480
|
+
* This wrapper acts as the intermediary between the Observable producer
|
|
481
|
+
* and the consumer-provided Observer.
|
|
482
|
+
*
|
|
483
|
+
* @typeParam T - The type of values delivered by the parent Observable.
|
|
484
|
+
*/
|
|
485
|
+
class SubscriptionObserver {
|
|
486
|
+
/**
|
|
487
|
+
* Returns whether this observer's subscription is closed.
|
|
488
|
+
*
|
|
489
|
+
* Uses the single source of truth for closed state from SubscriptionStateMap.
|
|
490
|
+
* This property is used by subscriber functions to check if they should
|
|
491
|
+
* continue delivering events.
|
|
492
|
+
*
|
|
493
|
+
* @example
|
|
494
|
+
* ```ts
|
|
495
|
+
* const timer = new Observable(observer => {
|
|
496
|
+
* const id = setInterval(() => {
|
|
497
|
+
* if (!observer.closed) {
|
|
498
|
+
* observer.next(Date.now());
|
|
499
|
+
* }
|
|
500
|
+
* }, 1000);
|
|
501
|
+
* return () => clearInterval(id);
|
|
502
|
+
* });
|
|
503
|
+
* ```
|
|
504
|
+
*/
|
|
505
|
+
get closed() {
|
|
506
|
+
const state = __classPrivateFieldGet(this, _SubscriptionObserver_state, "f");
|
|
507
|
+
if (!state)
|
|
508
|
+
return true;
|
|
509
|
+
return state.closed ?? true;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Creates a new SubscriptionObserver attached to the given subscription.
|
|
513
|
+
*
|
|
514
|
+
* @param subscription - The subscription that created this observer
|
|
515
|
+
*/
|
|
516
|
+
constructor(subscription) {
|
|
517
|
+
/** Cached state map to improve perf. */
|
|
518
|
+
_SubscriptionObserver_state.set(this, void 0);
|
|
519
|
+
/** Reference to the subscription that created this observer */
|
|
520
|
+
_SubscriptionObserver_subscription.set(this, null);
|
|
521
|
+
__classPrivateFieldSet(this, _SubscriptionObserver_subscription, subscription, "f");
|
|
522
|
+
if (subscription) {
|
|
523
|
+
__classPrivateFieldSet(this, _SubscriptionObserver_state, exports.SubscriptionStateMap.get(subscription), "f");
|
|
524
|
+
if (!__classPrivateFieldGet(this, _SubscriptionObserver_state, "f"))
|
|
525
|
+
throw new Error("Subscription state not found");
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Delivers the next value to the observer if the subscription is open.
|
|
530
|
+
*
|
|
531
|
+
* This is typically the "hot path" in an Observable implementation,
|
|
532
|
+
* as it's called for every emitted value. Key behaviors:
|
|
533
|
+
*
|
|
534
|
+
* 1. Silently returns if subscription is closed (no errors)
|
|
535
|
+
* 2. Properly preserves observer's `this` context
|
|
536
|
+
* 3. Catches and handles errors thrown from observer.next
|
|
537
|
+
* 4. Forwards errors to observer.error when available
|
|
538
|
+
*
|
|
539
|
+
* Performance Considerations:
|
|
540
|
+
* - Minimizes property access chains
|
|
541
|
+
* - Early returns for closed subscriptions
|
|
542
|
+
* - Type checking to avoid calling non-functions
|
|
543
|
+
*
|
|
544
|
+
* @param value - The value to deliver to the observer
|
|
545
|
+
*
|
|
546
|
+
* @example
|
|
547
|
+
* ```ts
|
|
548
|
+
* // Inside a subscriber function:
|
|
549
|
+
* observer.next(42); // Delivers value to consumer
|
|
550
|
+
* ```
|
|
551
|
+
*
|
|
552
|
+
* > Note: Error-propagation policy
|
|
553
|
+
* > ─────────────────────────────
|
|
554
|
+
* > * If the *observer supplies its own `error()` handler*,
|
|
555
|
+
* > that handler is considered the “catch-block” for the stream.
|
|
556
|
+
* > ↳ Any exception that happens *inside* the user’s `next()` /
|
|
557
|
+
* > `complete()` callbacks is forwarded to `error(err)` **once**.
|
|
558
|
+
* > ↳ If `error()` itself throws, we still delegate to `HostReportErrors` (≈ “unhandled-promise rejection”)
|
|
559
|
+
* > (i.e. `queueMicrotask`), exactly as the proposal specifies.
|
|
560
|
+
* >
|
|
561
|
+
* > * If the observer does **not** implement `error()`, we fall back to the
|
|
562
|
+
* > spec’s `HostReportErrors` behaviour (queueMicrotask + throw) so the host
|
|
563
|
+
* > surfaces the error just like an uncaught Promise rejection.
|
|
564
|
+
* >
|
|
565
|
+
* > Rationale – Think of `error()` as the moral equivalent of a `.catch()`
|
|
566
|
+
* > on a Promise. Once a catch exists, the host no longer warns about
|
|
567
|
+
* > “unhandled” rejections; we mirror that mental model here.
|
|
568
|
+
* >
|
|
569
|
+
* > Spec reference – This diverges slightly from stage-1, which still
|
|
570
|
+
* > invokes HostReportErrors if the *error handler itself* throws. We
|
|
571
|
+
* > intentionally suppress that extra surfacing for the reasons above.
|
|
572
|
+
*/
|
|
573
|
+
next(value) {
|
|
574
|
+
const state = __classPrivateFieldGet(this, _SubscriptionObserver_state, "f");
|
|
575
|
+
if (!state || state.closed)
|
|
576
|
+
return;
|
|
577
|
+
// Fast-path optimization to avoid long request chains
|
|
578
|
+
const observer = state.observer;
|
|
579
|
+
if (!observer)
|
|
580
|
+
return;
|
|
581
|
+
const nextFn = observer.next;
|
|
582
|
+
if (typeof nextFn !== "function")
|
|
583
|
+
return;
|
|
584
|
+
try {
|
|
585
|
+
nextFn.call(observer, value);
|
|
586
|
+
}
|
|
587
|
+
catch (err) {
|
|
588
|
+
const errorFn = observer.error;
|
|
589
|
+
if (typeof errorFn === "function") {
|
|
590
|
+
try {
|
|
591
|
+
errorFn.call(observer, err);
|
|
592
|
+
}
|
|
593
|
+
catch (err) {
|
|
594
|
+
queueMicrotask(() => {
|
|
595
|
+
throw err;
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
} // Either a user callback or HostReportErrors emulation (queueMicrotask).
|
|
599
|
+
else {
|
|
600
|
+
queueMicrotask(() => {
|
|
601
|
+
throw err;
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Delivers an error notification to the observer, then closes the subscription.
|
|
608
|
+
*
|
|
609
|
+
* Error is a terminal operation - after calling it:
|
|
610
|
+
* 1. The subscription is immediately marked as closed
|
|
611
|
+
* 2. Resources are released via unsubscribe()
|
|
612
|
+
* 3. No further notifications will be delivered
|
|
613
|
+
*
|
|
614
|
+
* Error Handling:
|
|
615
|
+
* - If observer.error exists, the error is delivered there
|
|
616
|
+
* - If observer.error throws, the error is reported asynchronously
|
|
617
|
+
* - If no error handler exists, the error is reported asynchronously
|
|
618
|
+
*
|
|
619
|
+
* > Note: Even for "silent" errors (no error handler), we still close
|
|
620
|
+
* the subscription and report the error to the host.
|
|
621
|
+
*
|
|
622
|
+
* ##
|
|
623
|
+
*
|
|
624
|
+
* @example Important Timing Consideration
|
|
625
|
+
* When this method is called during the subscriber function execution (before it returns),
|
|
626
|
+
* there's a potential race condition with cleanup functions.
|
|
627
|
+
*
|
|
628
|
+
* Consider:
|
|
629
|
+
* ```ts
|
|
630
|
+
* new Observable(observer => {
|
|
631
|
+
* observer.error(new Error()); // Triggers unsubscribe here
|
|
632
|
+
* return () => cleanupResources(); // But this hasn't been returned yet!
|
|
633
|
+
* });
|
|
634
|
+
* ```
|
|
635
|
+
*
|
|
636
|
+
* Our implementation handles this by:
|
|
637
|
+
* 1. Marking the subscription as closed immediately
|
|
638
|
+
* 2. Scheduling actual cleanup in a microtask to ensure the teardown function
|
|
639
|
+
* has time to be captured and stored
|
|
640
|
+
*
|
|
641
|
+
* This ensures resources are properly cleaned up even when error/complete
|
|
642
|
+
* is called synchronously during subscription setup.
|
|
643
|
+
*
|
|
644
|
+
* @param err - The error to deliver
|
|
645
|
+
*
|
|
646
|
+
* @example
|
|
647
|
+
* ```ts
|
|
648
|
+
* // Inside a subscriber function:
|
|
649
|
+
* try {
|
|
650
|
+
* doRiskyOperation();
|
|
651
|
+
* } catch (err) {
|
|
652
|
+
* observer.error(err); // Terminates the subscription with error
|
|
653
|
+
* }
|
|
654
|
+
* ```
|
|
655
|
+
*
|
|
656
|
+
* > Note: {@link SubscriptionObserver.next | Review the error propagation policy in `next()` on how errors propagate, the behaviour is not obvious on first glance.}
|
|
657
|
+
*/
|
|
658
|
+
error(err) {
|
|
659
|
+
const state = __classPrivateFieldGet(this, _SubscriptionObserver_state, "f");
|
|
660
|
+
// Mark closed and get observer in one call
|
|
661
|
+
const observer = markSubscriptionClosed(state, true);
|
|
662
|
+
if (observer === null)
|
|
663
|
+
return;
|
|
664
|
+
const errorFn = observer?.error;
|
|
665
|
+
if (typeof errorFn === "function") {
|
|
666
|
+
try {
|
|
667
|
+
errorFn.call(observer, err);
|
|
668
|
+
}
|
|
669
|
+
catch (innerErr) {
|
|
670
|
+
queueMicrotask(() => {
|
|
671
|
+
throw innerErr;
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
} // No error handler, delegate to host
|
|
675
|
+
else {
|
|
676
|
+
queueMicrotask(() => {
|
|
677
|
+
throw err;
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
// Perform cleanup after marking closed
|
|
681
|
+
performSubscriptionCleanup(__classPrivateFieldGet(this, _SubscriptionObserver_subscription, "f"), state);
|
|
682
|
+
// Clear reference
|
|
683
|
+
__classPrivateFieldSet(this, _SubscriptionObserver_subscription, null, "f");
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Signals successful completion of the observable sequence.
|
|
687
|
+
*
|
|
688
|
+
* Complete is a terminal operation - after calling it:
|
|
689
|
+
* 1. The subscription is immediately marked as closed
|
|
690
|
+
* 2. Resources are released via unsubscribe()
|
|
691
|
+
* 3. No further notifications will be delivered
|
|
692
|
+
*
|
|
693
|
+
* If observer.complete throws an error:
|
|
694
|
+
* - The error is forwarded to observer.error if available
|
|
695
|
+
* - Otherwise, it's reported asynchronously to the host
|
|
696
|
+
*
|
|
697
|
+
* @example
|
|
698
|
+
* ```ts
|
|
699
|
+
* // Inside a subscriber function:
|
|
700
|
+
* observer.next(1);
|
|
701
|
+
* observer.next(2);
|
|
702
|
+
* observer.complete(); // Terminates the subscription normally
|
|
703
|
+
* ```
|
|
704
|
+
*
|
|
705
|
+
* > Note: {@link SubscriptionObserver.next | Review the error propagation policy in `next()` on how errors propagate, the behaviour is not obvious on first glance.}
|
|
706
|
+
*/
|
|
707
|
+
complete() {
|
|
708
|
+
const state = __classPrivateFieldGet(this, _SubscriptionObserver_state, "f");
|
|
709
|
+
if (!state || state.closed)
|
|
710
|
+
return;
|
|
711
|
+
// Mark closed and get observer in one call
|
|
712
|
+
const observer = markSubscriptionClosed(state, true);
|
|
713
|
+
if (observer === null)
|
|
714
|
+
return;
|
|
715
|
+
const completeFn = observer?.complete;
|
|
716
|
+
if (typeof completeFn === "function") {
|
|
717
|
+
try {
|
|
718
|
+
completeFn.call(observer);
|
|
719
|
+
}
|
|
720
|
+
catch (err) {
|
|
721
|
+
const errorFn = observer?.error;
|
|
722
|
+
if (typeof errorFn === "function") {
|
|
723
|
+
try {
|
|
724
|
+
errorFn.call(observer, err);
|
|
725
|
+
}
|
|
726
|
+
catch (innerErr) {
|
|
727
|
+
queueMicrotask(() => {
|
|
728
|
+
throw innerErr;
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
} // Either a user callback or HostReportErrors emulation (queueMicrotask).
|
|
732
|
+
else {
|
|
733
|
+
queueMicrotask(() => {
|
|
734
|
+
throw err;
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
// Perform cleanup after marking closed
|
|
740
|
+
performSubscriptionCleanup(__classPrivateFieldGet(this, _SubscriptionObserver_subscription, "f"), state);
|
|
741
|
+
// Clear reference
|
|
742
|
+
__classPrivateFieldSet(this, _SubscriptionObserver_subscription, null, "f");
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Returns a standard string tag for the object.
|
|
746
|
+
* Used by Object.prototype.toString.
|
|
747
|
+
*/
|
|
748
|
+
get [(_SubscriptionObserver_state = new WeakMap(), _SubscriptionObserver_subscription = new WeakMap(), symbol_js_1.Symbol.toStringTag)]() {
|
|
749
|
+
return "Subscription Observer";
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
exports.SubscriptionObserver = SubscriptionObserver;
|
|
753
|
+
/**
|
|
754
|
+
* Observale - A push-based stream for handling async data over time.
|
|
755
|
+
*
|
|
756
|
+
* **What it is**: Like a "smart Promise" that can emit multiple values and provides
|
|
757
|
+
* unified patterns for resource management, error handling, and subscription lifecycle.
|
|
758
|
+
*
|
|
759
|
+
* Observable is the central type in this library, representing a push-based
|
|
760
|
+
* source of values that can be subscribed to. It delivers values to observers
|
|
761
|
+
* and provides lifecycle guarantees around subscription and cleanup.
|
|
762
|
+
*
|
|
763
|
+
* Key guarantees:
|
|
764
|
+
* 1. Lazy execution - nothing happens until `subscribe()` is called
|
|
765
|
+
* 2. Multiple independent subscriptions to the same Observable
|
|
766
|
+
* 3. Each subscriber executes and cleans up independently.
|
|
767
|
+
* 4. Cleanups are deterministic one‑time resource disposal, the occur when subscriptions are cancelled, error or complete
|
|
768
|
+
*
|
|
769
|
+
* Extensions beyond the TC39 proposal:
|
|
770
|
+
* - Pull API via AsyncIterable interface
|
|
771
|
+
* - Using/await using support via Symbol.dispose/asyncDispose
|
|
772
|
+
*
|
|
773
|
+
* Gotchas:
|
|
774
|
+
* - Two subscribers → two side‑effects on a cold stream.
|
|
775
|
+
* - Remember to cancel infinite observables.
|
|
776
|
+
* - Calling `next()` after `complete()` is a no‑op.
|
|
777
|
+
* - Errors in observer callbacks go to error handler if provided, else global reporting.
|
|
778
|
+
* - Synchronous completion during subscribe still captures cleanup functions.
|
|
779
|
+
*
|
|
780
|
+
* @typeParam T - Type of values emitted by this Observable
|
|
781
|
+
*/
|
|
782
|
+
class Observable {
|
|
783
|
+
/**
|
|
784
|
+
* Creates a new Observable with the given subscriber function.
|
|
785
|
+
*
|
|
786
|
+
* **Important**: This just stores your function - nothing executes until `subscribe()` is called.
|
|
787
|
+
* Think of it like writing a recipe vs actually cooking.
|
|
788
|
+
*
|
|
789
|
+
* The subscriber function is the heart of an Observable. It:
|
|
790
|
+
* 1. Is called once per subscription (not at Observable creation time)
|
|
791
|
+
* 2. Receives a SubscriptionObserver to send values through
|
|
792
|
+
* 3. Can optionally return a cleanup function or subscription
|
|
793
|
+
*
|
|
794
|
+
* Nothing happens when an Observable is created - execution only
|
|
795
|
+
* begins when subscribe() is called.
|
|
796
|
+
*
|
|
797
|
+
* @param subscribeFn - Function that implements the Observable's behavior
|
|
798
|
+
*
|
|
799
|
+
* Your subscriber function receives a `SubscriptionObserver` to:
|
|
800
|
+
* - `observer.next(value)` - Emit a value
|
|
801
|
+
* - `observer.error(err)` - Emit error (terminates)
|
|
802
|
+
* - `observer.complete()` - Signal completion (terminates)
|
|
803
|
+
* - `observer.closed` - Check if subscription is still active
|
|
804
|
+
*
|
|
805
|
+
* @throws TypeError if subscribeFn is not a function
|
|
806
|
+
* @throws TypeError if Observable is called without "new"
|
|
807
|
+
*
|
|
808
|
+
* @example Timer with cleanup
|
|
809
|
+
* ```ts
|
|
810
|
+
* // Timer that emits the current timestamp every second
|
|
811
|
+
* const timer = new Observable(observer => {
|
|
812
|
+
* console.log('Subscription started!');
|
|
813
|
+
* const id = setInterval(() => {
|
|
814
|
+
* observer.next(Date.now());
|
|
815
|
+
* }, 1000);
|
|
816
|
+
*
|
|
817
|
+
* // Return cleanup function
|
|
818
|
+
* return () => {
|
|
819
|
+
* console.log('Cleaning up timer');
|
|
820
|
+
* clearInterval(id);
|
|
821
|
+
* };
|
|
822
|
+
* });
|
|
823
|
+
* ```
|
|
824
|
+
*
|
|
825
|
+
* @example Async operation with error handling
|
|
826
|
+
* ```ts
|
|
827
|
+
* const fetch = new Observable(observer => {
|
|
828
|
+
* const controller = new AbortController();
|
|
829
|
+
*
|
|
830
|
+
* fetch('/api/data', { signal: controller.signal })
|
|
831
|
+
* .then(res => res.json())
|
|
832
|
+
* .then(data => {
|
|
833
|
+
* observer.next(data);
|
|
834
|
+
* observer.complete();
|
|
835
|
+
* })
|
|
836
|
+
* .catch(err => observer.error(err));
|
|
837
|
+
*
|
|
838
|
+
* return () => controller.abort(); // Cleanup
|
|
839
|
+
* });
|
|
840
|
+
* ```
|
|
841
|
+
*/
|
|
842
|
+
constructor(subscribeFn) {
|
|
843
|
+
/** The subscriber function provided when the Observable was created */
|
|
844
|
+
_Observable_subscribeFn.set(this, void 0);
|
|
845
|
+
if (typeof subscribeFn !== "function") {
|
|
846
|
+
throw new TypeError("Observable initializer must be a function");
|
|
847
|
+
}
|
|
848
|
+
// Add check for constructor invocation
|
|
849
|
+
if (!(this instanceof Observable)) {
|
|
850
|
+
throw new TypeError("Observable must be called with new");
|
|
851
|
+
}
|
|
852
|
+
__classPrivateFieldSet(this, _Observable_subscribeFn, subscribeFn, "f");
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Returns this Observable (required for interoperability).
|
|
856
|
+
*
|
|
857
|
+
* This method implements the TC39 Symbol.observable protocol,
|
|
858
|
+
* which allows foreign Observable implementations to recognize
|
|
859
|
+
* and interoperate with this implementation.
|
|
860
|
+
*
|
|
861
|
+
* @returns This Observable instance
|
|
862
|
+
*/
|
|
863
|
+
[(_Observable_subscribeFn = new WeakMap(), symbol_js_1.Symbol.observable)]() {
|
|
864
|
+
return this;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Implementation of subscribe method (handles both overloads).
|
|
868
|
+
*/
|
|
869
|
+
subscribe(observerOrNext, errorOrOpts, complete, _opts) {
|
|
870
|
+
// Check for invalid this context
|
|
871
|
+
if (this === null || this === undefined) {
|
|
872
|
+
throw new TypeError('Cannot read property "subscribe" of null or undefined');
|
|
873
|
+
}
|
|
874
|
+
/* -------------------------------------------------------------------
|
|
875
|
+
* 1. Normalise the observer – mirrors spec step 4.
|
|
876
|
+
* ------------------------------------------------------------------- */
|
|
877
|
+
const observer = (typeof observerOrNext === "function"
|
|
878
|
+
? {
|
|
879
|
+
next: observerOrNext,
|
|
880
|
+
error: errorOrOpts,
|
|
881
|
+
complete,
|
|
882
|
+
}
|
|
883
|
+
: observerOrNext) ?? {}; // ← spec-compliant fallback for null / primitives
|
|
884
|
+
// Additional options to pass along AbortSignal (part of the WCIG Observables Spec., thought to implement it for convinence reasons)
|
|
885
|
+
const opts = (typeof observerOrNext === "function"
|
|
886
|
+
? _opts
|
|
887
|
+
: errorOrOpts) ?? {};
|
|
888
|
+
/* -------------------------------------------------------------------
|
|
889
|
+
* 2. Create the Subscription facade (spec: CreateSubscription()).
|
|
890
|
+
* ------------------------------------------------------------------- */
|
|
891
|
+
const subscription = createSubscription(observer, opts);
|
|
892
|
+
/* -------------------------------------------------------------------
|
|
893
|
+
* 3. Wrap user observer so we enforce closed-state.
|
|
894
|
+
* ------------------------------------------------------------------- */
|
|
895
|
+
const subObserver = new SubscriptionObserver(subscription);
|
|
896
|
+
/* -------------------------------------------------------------------
|
|
897
|
+
* 4. Call observer.start(subscription) – (spec step 10).
|
|
898
|
+
* ------------------------------------------------------------------- */
|
|
899
|
+
try {
|
|
900
|
+
observer.start?.(subscription);
|
|
901
|
+
if (subscription?.closed)
|
|
902
|
+
return subscription; // spec step 10.d
|
|
903
|
+
}
|
|
904
|
+
catch (err) {
|
|
905
|
+
// WarnIfAbrupt: report, but return closed subscription
|
|
906
|
+
// Queue in a micro-task so it surfaces *after* current job,
|
|
907
|
+
// matching the spec’s “report later” intent.
|
|
908
|
+
queueMicrotask(() => {
|
|
909
|
+
// 1. Print to console for visibility
|
|
910
|
+
console.error(err);
|
|
911
|
+
// 2. Re-throw so debuggers break (optional, but common)
|
|
912
|
+
throw err;
|
|
913
|
+
});
|
|
914
|
+
subscription?.unsubscribe?.();
|
|
915
|
+
return subscription;
|
|
916
|
+
}
|
|
917
|
+
/* -------------------------------------------------------------------
|
|
918
|
+
* 5. Execute the user subscriber and capture its cleanup (spec step 12-16).
|
|
919
|
+
* ------------------------------------------------------------------- */
|
|
920
|
+
try {
|
|
921
|
+
let cleanup = __classPrivateFieldGet(this, _Observable_subscribeFn, "f")?.call(undefined, subObserver) ?? null;
|
|
922
|
+
// Validate the cleanup value if provided
|
|
923
|
+
if (cleanup !== undefined && cleanup !== null) {
|
|
924
|
+
if (!(typeof cleanup === "function" ||
|
|
925
|
+
typeof cleanup?.unsubscribe === "function" ||
|
|
926
|
+
typeof cleanup?.[symbol_js_1.Symbol.dispose] === "function" ||
|
|
927
|
+
typeof cleanup?.[symbol_js_1.Symbol.asyncDispose] ===
|
|
928
|
+
"function")) {
|
|
929
|
+
throw new TypeError("Expected subscriber to return a function, an unsubscribe object, a disposable with a [Symbol.dispose] method, an async-disposable with a [Symbol.asyncDispose] method, or undefined/null");
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
// Store the cleanup function in the subscription state
|
|
933
|
+
const state = exports.SubscriptionStateMap.get(subscription);
|
|
934
|
+
if (state && cleanup)
|
|
935
|
+
state.cleanup = cleanup;
|
|
936
|
+
/**
|
|
937
|
+
* Handle the case where complete/error was called synchronously during the subscribe function.
|
|
938
|
+
* This is a critical edge case that requires special handling - when the observer
|
|
939
|
+
* calls `error()` or `complete()` before the subscribe function returns, we need to ensure
|
|
940
|
+
* that any teardown function returned by the subscriber is still executed properly.
|
|
941
|
+
*
|
|
942
|
+
* The returned teardown wouldn't have been available when `unsubscribe()` was initially
|
|
943
|
+
* triggered by error/complete, so we need to handle it manually here.
|
|
944
|
+
*
|
|
945
|
+
* @example
|
|
946
|
+
* ```ts
|
|
947
|
+
* const errorObservable = new Observable(observer => {
|
|
948
|
+
* observer.error(new Error("test error")); // Will auto-unsubscribe (but teardown hasn't been defined yet)
|
|
949
|
+
* log.push("after error"); // This should still run
|
|
950
|
+
*
|
|
951
|
+
* // Teardown now defined but now the subscription has been closedn
|
|
952
|
+
* // but resources being used haven't actually been disposed yet
|
|
953
|
+
* return () => {
|
|
954
|
+
* log.push("error teardown");
|
|
955
|
+
* };
|
|
956
|
+
* });
|
|
957
|
+
* ```
|
|
958
|
+
*
|
|
959
|
+
* `observer.error` fires before the teardown function is defined, so we would need to manually cleanup ourselves
|
|
960
|
+
* by manually running the teardown function
|
|
961
|
+
*/
|
|
962
|
+
if (subscription.closed && cleanup) {
|
|
963
|
+
cleanupSubscription(cleanup);
|
|
964
|
+
}
|
|
965
|
+
cleanup = null;
|
|
966
|
+
}
|
|
967
|
+
catch (err) {
|
|
968
|
+
// 6) If their subscribeFn throws, send that as an error notification
|
|
969
|
+
subObserver.error(err);
|
|
970
|
+
}
|
|
971
|
+
// 7) Finally, hand back the Subscription so callers can cancel whenever they like
|
|
972
|
+
return subscription;
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Enables `for await ... of observable` syntax for direct async iteration.
|
|
976
|
+
*
|
|
977
|
+
* This method allows Observables to be used in any context that accepts an AsyncIterable,
|
|
978
|
+
* implementing the "pull" mode of consuming an Observable.
|
|
979
|
+
*
|
|
980
|
+
* Uses default buffer size of 64 items.
|
|
981
|
+
*
|
|
982
|
+
* The implementation delegates to the `pull()` function which:
|
|
983
|
+
* 1. Converts push-based events to pull-based async iteration
|
|
984
|
+
* 2. Applies backpressure with ReadableStream
|
|
985
|
+
* 3. Handles proper cleanup on early termination
|
|
986
|
+
*
|
|
987
|
+
* @returns An AsyncIterator that yields values from this Observable
|
|
988
|
+
*
|
|
989
|
+
* @example
|
|
990
|
+
* ```ts
|
|
991
|
+
* const observable = Observable.of(1, 2, 3);
|
|
992
|
+
*
|
|
993
|
+
* // Using for-await-of directly on an Observable
|
|
994
|
+
* for await (const value of observable) {
|
|
995
|
+
* console.log(value); // Logs 1, 2, 3
|
|
996
|
+
* }
|
|
997
|
+
* ```
|
|
998
|
+
*/
|
|
999
|
+
async *[symbol_js_1.Symbol.asyncIterator]() {
|
|
1000
|
+
yield* pull(this);
|
|
1001
|
+
}
|
|
1002
|
+
pull(opts) {
|
|
1003
|
+
return pull(this, opts);
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Standard string tag for the object.
|
|
1007
|
+
* Used by Object.prototype.toString.
|
|
1008
|
+
*/
|
|
1009
|
+
get [symbol_js_1.Symbol.toStringTag]() {
|
|
1010
|
+
return "Observable";
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
exports.Observable = Observable;
|
|
1014
|
+
/**
|
|
1015
|
+
* Converts Promise, an iterable, async iterable, or Observable-like object to an Observable.
|
|
1016
|
+
*
|
|
1017
|
+
* This static method is a key part of the Observable interoperability mechanism,
|
|
1018
|
+
* handling multiple input types in a consistent way.
|
|
1019
|
+
*
|
|
1020
|
+
* **Handles**:
|
|
1021
|
+
* - Arrays, Sets, Maps → sync emission
|
|
1022
|
+
* - Async generators → values over time
|
|
1023
|
+
* - Symbol.observable objects → delegates to their implementation
|
|
1024
|
+
*
|
|
1025
|
+
* Behavior depends on the input type:
|
|
1026
|
+
* 1. Objects with Symbol.observable - Delegates to their implementation
|
|
1027
|
+
* 2. Synchronous iterables - Emits all values then completes
|
|
1028
|
+
* 3. Asynchronous iterables - Emits values as they arrive then completes
|
|
1029
|
+
* 4. Promise - Emits a single value (the resovled value) then completes
|
|
1030
|
+
*
|
|
1031
|
+
* Unlike Promise.resolve, Observable.from will not return the input unchanged
|
|
1032
|
+
* if it's already an Observable, unless it's an instance of the exact same
|
|
1033
|
+
* constructor. This ensures consistent behavior across different Observable
|
|
1034
|
+
* implementations.
|
|
1035
|
+
*
|
|
1036
|
+
* @param input - The object to convert to an Observable
|
|
1037
|
+
* @returns A new Observable that emits values from the input
|
|
1038
|
+
*
|
|
1039
|
+
* @example
|
|
1040
|
+
* ```ts
|
|
1041
|
+
* // From an array
|
|
1042
|
+
* Observable.from([1, 2, 3]).subscribe({
|
|
1043
|
+
* next: val => console.log(val) // 1, 2, 3
|
|
1044
|
+
* });
|
|
1045
|
+
*
|
|
1046
|
+
* // From a Promise
|
|
1047
|
+
* Observable.from(Promise.resolve("result")).subscribe({
|
|
1048
|
+
* next: val => console.log(val) // "result"
|
|
1049
|
+
* });
|
|
1050
|
+
*
|
|
1051
|
+
* // From another Observable-like object
|
|
1052
|
+
* const foreign = {
|
|
1053
|
+
* [Symbol.observable]() {
|
|
1054
|
+
* return new Observable(obs => {
|
|
1055
|
+
* obs.next("hello");
|
|
1056
|
+
* obs.complete();
|
|
1057
|
+
* });
|
|
1058
|
+
* }
|
|
1059
|
+
* };
|
|
1060
|
+
* Observable.from(foreign).subscribe({
|
|
1061
|
+
* next: val => console.log(val) // "hello"
|
|
1062
|
+
* });
|
|
1063
|
+
* ```
|
|
1064
|
+
*/
|
|
1065
|
+
Object.defineProperty(Observable, "from", {
|
|
1066
|
+
enumerable: true,
|
|
1067
|
+
configurable: true,
|
|
1068
|
+
writable: true,
|
|
1069
|
+
value: from
|
|
1070
|
+
});
|
|
1071
|
+
/**
|
|
1072
|
+
* Creates an Observable that synchronously emits the given values then completes.
|
|
1073
|
+
*
|
|
1074
|
+
* This is a convenience method for creating simple Observables that:
|
|
1075
|
+
* 1. Emit a fixed set of values synchronously
|
|
1076
|
+
* 2. Complete immediately after emitting all values
|
|
1077
|
+
* 3. Never error
|
|
1078
|
+
*
|
|
1079
|
+
* It's the Observable equivalent of `Promise.resolve()` for single values
|
|
1080
|
+
* or `[].values()` for multiple values.
|
|
1081
|
+
*
|
|
1082
|
+
* @param items - Values to emit
|
|
1083
|
+
* @returns A new Observable that emits the given values then completes
|
|
1084
|
+
*
|
|
1085
|
+
* @example
|
|
1086
|
+
* ```ts
|
|
1087
|
+
* // Create and subscribe
|
|
1088
|
+
* Observable.of(1, 2, 3).subscribe({
|
|
1089
|
+
* next: val => console.log(val), // Logs 1, 2, 3
|
|
1090
|
+
* complete: () => console.log('Done!')
|
|
1091
|
+
* });
|
|
1092
|
+
*
|
|
1093
|
+
* // Output:
|
|
1094
|
+
* // 1
|
|
1095
|
+
* // 2
|
|
1096
|
+
* // 3
|
|
1097
|
+
* // Done!
|
|
1098
|
+
* ```
|
|
1099
|
+
*/
|
|
1100
|
+
Object.defineProperty(Observable, "of", {
|
|
1101
|
+
enumerable: true,
|
|
1102
|
+
configurable: true,
|
|
1103
|
+
writable: true,
|
|
1104
|
+
value: of
|
|
1105
|
+
});
|
|
1106
|
+
/**
|
|
1107
|
+
* Converts a Observable into an AsyncGenerator with backpressure control.
|
|
1108
|
+
*
|
|
1109
|
+
* This method provides more control over async iteration than the default
|
|
1110
|
+
* Symbol.asyncIterator implementation, allowing consumers to:
|
|
1111
|
+
*
|
|
1112
|
+
* 1. Specify a queuing strategy with a custom highWaterMark
|
|
1113
|
+
* 2. Control buffering behavior when the producer is faster than the consumer
|
|
1114
|
+
* 3. Apply backpressure to prevent memory issues with fast producers
|
|
1115
|
+
*
|
|
1116
|
+
* The implementation uses ReadableStream internally to manage buffering
|
|
1117
|
+
* and backpressure, pausing the producer when the buffer fills up.
|
|
1118
|
+
*
|
|
1119
|
+
* @param options - Configuration options for the pull operation
|
|
1120
|
+
* @returns An AsyncGenerator that yields values from this Observable
|
|
1121
|
+
*
|
|
1122
|
+
* @example
|
|
1123
|
+
* ```ts
|
|
1124
|
+
* // Buffer up to 5 items before applying backpressure
|
|
1125
|
+
* for await (const value of observable.pull({
|
|
1126
|
+
* strategy: { highWaterMark: 5 }
|
|
1127
|
+
* })) {
|
|
1128
|
+
* console.log(value);
|
|
1129
|
+
* // Slow consumer - producer will pause when buffer fills
|
|
1130
|
+
* await new Promise(r => setTimeout(r, 1000));
|
|
1131
|
+
* }
|
|
1132
|
+
* ```
|
|
1133
|
+
*/
|
|
1134
|
+
Object.defineProperty(Observable, "pull", {
|
|
1135
|
+
enumerable: true,
|
|
1136
|
+
configurable: true,
|
|
1137
|
+
writable: true,
|
|
1138
|
+
value: pull
|
|
1139
|
+
});
|
|
1140
|
+
/**
|
|
1141
|
+
* Cached empty observable handler
|
|
1142
|
+
* @internal
|
|
1143
|
+
*/
|
|
1144
|
+
function EMPTY(obs) {
|
|
1145
|
+
obs.complete();
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Creates an Observable that synchronously emits the given values then completes.
|
|
1149
|
+
*
|
|
1150
|
+
* This standalone function implements the Observable.of static method while
|
|
1151
|
+
* properly supporting subclassing. It's the Observable equivalent of:
|
|
1152
|
+
* - `Array.of()` for collections
|
|
1153
|
+
* - `Promise.resolve()` for single values
|
|
1154
|
+
*
|
|
1155
|
+
* Key behaviors:
|
|
1156
|
+
* 1. Emits values synchronously when subscribed
|
|
1157
|
+
* 2. Completes immediately after all values are emitted
|
|
1158
|
+
* 3. Never errors
|
|
1159
|
+
* 4. Respects the constructor it was called on for subclassing
|
|
1160
|
+
*
|
|
1161
|
+
* @param items - Values to emit
|
|
1162
|
+
* @returns A new Observable that emits the given values then completes
|
|
1163
|
+
*
|
|
1164
|
+
* @example
|
|
1165
|
+
* ```ts
|
|
1166
|
+
* // Basic usage
|
|
1167
|
+
* of(1, 2, 3).subscribe({
|
|
1168
|
+
* next: x => console.log(x),
|
|
1169
|
+
* complete: () => console.log('Done!')
|
|
1170
|
+
* });
|
|
1171
|
+
* // Output: 1, 2, 3, Done!
|
|
1172
|
+
*
|
|
1173
|
+
* // Subclassing support
|
|
1174
|
+
* class MyObservable extends Observable<number> {
|
|
1175
|
+
* // Custom methods...
|
|
1176
|
+
* }
|
|
1177
|
+
*
|
|
1178
|
+
* // Creates a MyObservable instance
|
|
1179
|
+
* const mine = MyObservable.of(1, 2, 3);
|
|
1180
|
+
* ```
|
|
1181
|
+
*/
|
|
1182
|
+
function of(...items) {
|
|
1183
|
+
const Constructor = typeof this === "function"
|
|
1184
|
+
? this
|
|
1185
|
+
: Observable;
|
|
1186
|
+
const len = items.length;
|
|
1187
|
+
// Pre-defined handlers for common cases to avoid creating new closures
|
|
1188
|
+
switch (len) {
|
|
1189
|
+
case 0:
|
|
1190
|
+
return new Constructor(EMPTY);
|
|
1191
|
+
case 1:
|
|
1192
|
+
return new Constructor((obs) => {
|
|
1193
|
+
obs.next(items[0]);
|
|
1194
|
+
obs.complete();
|
|
1195
|
+
});
|
|
1196
|
+
case 2:
|
|
1197
|
+
return new Constructor((obs) => {
|
|
1198
|
+
obs.next(items[0]);
|
|
1199
|
+
obs.next(items[1]);
|
|
1200
|
+
obs.complete();
|
|
1201
|
+
});
|
|
1202
|
+
case 3:
|
|
1203
|
+
return new Constructor((obs) => {
|
|
1204
|
+
obs.next(items[0]);
|
|
1205
|
+
obs.next(items[1]);
|
|
1206
|
+
obs.next(items[2]);
|
|
1207
|
+
obs.complete();
|
|
1208
|
+
});
|
|
1209
|
+
default:
|
|
1210
|
+
// For arrays > 3 items, balance between code size and performance
|
|
1211
|
+
return new Constructor((obs) => {
|
|
1212
|
+
// Based on benchmarking:
|
|
1213
|
+
// - Arrays < 100: simple loop is fine (method call dominates)
|
|
1214
|
+
// - Arrays >= 100: unrolling provides measurable benefit
|
|
1215
|
+
if (len < 100) {
|
|
1216
|
+
for (let i = 0; i < len; i++) {
|
|
1217
|
+
obs.next(items[i]);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
else {
|
|
1221
|
+
// Unroll by 8 for large arrays (2.8x speedup)
|
|
1222
|
+
let i = 0;
|
|
1223
|
+
const limit = len - (len % 8);
|
|
1224
|
+
for (; i < limit; i += 8) {
|
|
1225
|
+
obs.next(items[i]);
|
|
1226
|
+
obs.next(items[i + 1]);
|
|
1227
|
+
obs.next(items[i + 2]);
|
|
1228
|
+
obs.next(items[i + 3]);
|
|
1229
|
+
obs.next(items[i + 4]);
|
|
1230
|
+
obs.next(items[i + 5]);
|
|
1231
|
+
obs.next(items[i + 6]);
|
|
1232
|
+
obs.next(items[i + 7]);
|
|
1233
|
+
}
|
|
1234
|
+
// Handle remainder
|
|
1235
|
+
for (; i < len; i++) {
|
|
1236
|
+
obs.next(items[i]);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
obs.complete();
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Converts an Observable-like, sync iterable, or async iterable into an Observable.
|
|
1245
|
+
*
|
|
1246
|
+
* This is the standalone implementation of Observable.from, supporting:
|
|
1247
|
+
* - Objects with Symbol.observable (Observable-like)
|
|
1248
|
+
* - Regular iterables (arrays, Maps, Sets, generators)
|
|
1249
|
+
* - Async iterables (async generators, ReadableStreams)
|
|
1250
|
+
*
|
|
1251
|
+
* Conversion follows these rules:
|
|
1252
|
+
* 1. For Symbol.observable objects: delegates to their implementation
|
|
1253
|
+
* 2. For Promises: resolves and emits the promise's value
|
|
1254
|
+
* 3. For iterables: synchronously emits all values, then completes
|
|
1255
|
+
* 4. For async iterables: emits values as they arrive, then completes
|
|
1256
|
+
*
|
|
1257
|
+
* This function properly supports subclassing, preserving the constructor
|
|
1258
|
+
* it was called on.
|
|
1259
|
+
*
|
|
1260
|
+
* @throws TypeError if input is null, undefined, or not convertible
|
|
1261
|
+
*
|
|
1262
|
+
* @example
|
|
1263
|
+
* ```ts
|
|
1264
|
+
* // From array
|
|
1265
|
+
* from([1, 2, 3]).subscribe(x => console.log(x));
|
|
1266
|
+
* // Output: 1, 2, 3
|
|
1267
|
+
*
|
|
1268
|
+
* // From Promise
|
|
1269
|
+
* from(Promise.resolve('done')).subscribe(x => console.log(x));
|
|
1270
|
+
* // Output: 'done'
|
|
1271
|
+
*
|
|
1272
|
+
* // From Map
|
|
1273
|
+
* from(new Map([['a', 1], ['b', 2]])).subscribe(x => console.log(x));
|
|
1274
|
+
* // Output: ['a', 1], ['b', 2]
|
|
1275
|
+
*
|
|
1276
|
+
* // From another Observable implementation
|
|
1277
|
+
* const foreign = {
|
|
1278
|
+
* [Symbol.observable]() {
|
|
1279
|
+
* return { subscribe: observer => {
|
|
1280
|
+
* observer.next('hello');
|
|
1281
|
+
* observer.complete();
|
|
1282
|
+
* return { unsubscribe() {} };
|
|
1283
|
+
* }};
|
|
1284
|
+
* }
|
|
1285
|
+
* };
|
|
1286
|
+
* from(foreign).subscribe(x => console.log(x));
|
|
1287
|
+
* // Output: 'hello'
|
|
1288
|
+
* ```
|
|
1289
|
+
*/
|
|
1290
|
+
function from(input, { throwError = true } = {}) {
|
|
1291
|
+
if (input === null || input === undefined) {
|
|
1292
|
+
throw new TypeError("Cannot convert undefined or null to Observable");
|
|
1293
|
+
}
|
|
1294
|
+
const Constructor = typeof this === "function"
|
|
1295
|
+
? this
|
|
1296
|
+
: Observable;
|
|
1297
|
+
// Faster implementation of iteration for array-like values
|
|
1298
|
+
const arr = input;
|
|
1299
|
+
if (Array.isArray(input) || typeof arr.length === "number") {
|
|
1300
|
+
const len = arr.length;
|
|
1301
|
+
// Optimize for small arrays
|
|
1302
|
+
if (len === 0)
|
|
1303
|
+
return new Constructor(EMPTY);
|
|
1304
|
+
if (len === 1) {
|
|
1305
|
+
return new Constructor((obs) => {
|
|
1306
|
+
if (throwError)
|
|
1307
|
+
(0, error_js_1.assertObservableError)(arr[0], obs);
|
|
1308
|
+
obs.next(arr[0]);
|
|
1309
|
+
obs.complete();
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
// Type check to ensure it's actually array-like
|
|
1313
|
+
return new Constructor((obs) => {
|
|
1314
|
+
try {
|
|
1315
|
+
// Typed arrays: no bounds checking needed, direct iteration
|
|
1316
|
+
// Small arrays: simple loop with early exit checks
|
|
1317
|
+
if (len < 100 || ArrayBuffer.isView(arr)) {
|
|
1318
|
+
for (let i = 0; i < len; i++) {
|
|
1319
|
+
if (throwError)
|
|
1320
|
+
(0, error_js_1.assertObservableError)(arr[i]);
|
|
1321
|
+
obs.next(arr[i]);
|
|
1322
|
+
if (obs.closed)
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
else {
|
|
1327
|
+
// Large arrays: unroll with less frequent closed checks
|
|
1328
|
+
let i = 0;
|
|
1329
|
+
const limit = len - (len % 8);
|
|
1330
|
+
// Check closed once per 8 items (balanced approach)
|
|
1331
|
+
for (; i < limit; i += 8) {
|
|
1332
|
+
if (throwError) {
|
|
1333
|
+
(0, error_js_1.assertObservableError)(arr[i]);
|
|
1334
|
+
(0, error_js_1.assertObservableError)(arr[i + 1]);
|
|
1335
|
+
(0, error_js_1.assertObservableError)(arr[i + 2]);
|
|
1336
|
+
(0, error_js_1.assertObservableError)(arr[i + 3]);
|
|
1337
|
+
(0, error_js_1.assertObservableError)(arr[i + 4]);
|
|
1338
|
+
(0, error_js_1.assertObservableError)(arr[i + 5]);
|
|
1339
|
+
(0, error_js_1.assertObservableError)(arr[i + 6]);
|
|
1340
|
+
(0, error_js_1.assertObservableError)(arr[i + 7]);
|
|
1341
|
+
}
|
|
1342
|
+
obs.next(arr[i]);
|
|
1343
|
+
obs.next(arr[i + 1]);
|
|
1344
|
+
obs.next(arr[i + 2]);
|
|
1345
|
+
obs.next(arr[i + 3]);
|
|
1346
|
+
obs.next(arr[i + 4]);
|
|
1347
|
+
obs.next(arr[i + 5]);
|
|
1348
|
+
obs.next(arr[i + 6]);
|
|
1349
|
+
obs.next(arr[i + 7]);
|
|
1350
|
+
if (obs.closed)
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
// Handle remainder with checks
|
|
1354
|
+
for (; i < len; i++) {
|
|
1355
|
+
if (throwError)
|
|
1356
|
+
(0, error_js_1.assertObservableError)(arr[i]);
|
|
1357
|
+
obs.next(arr[i]);
|
|
1358
|
+
if (obs.closed)
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
catch (err) {
|
|
1364
|
+
obs.error(err);
|
|
1365
|
+
}
|
|
1366
|
+
obs.complete();
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
// Case 1 – object with @@observable
|
|
1370
|
+
const observableFn = input[symbol_js_1.Symbol.observable];
|
|
1371
|
+
if (typeof observableFn === "function") {
|
|
1372
|
+
const observable = observableFn.call(input);
|
|
1373
|
+
// Validate the result has a subscribe method
|
|
1374
|
+
if (!observable || typeof observable.subscribe !== "function") {
|
|
1375
|
+
throw new TypeError("Object returned from [Symbol.observable]() does not implement subscribe method");
|
|
1376
|
+
}
|
|
1377
|
+
// Return directly if it's already an instance of the target constructor
|
|
1378
|
+
if (observable instanceof Constructor)
|
|
1379
|
+
return observable;
|
|
1380
|
+
// Otherwise, wrap it to ensure consistent behavior
|
|
1381
|
+
return new Constructor((observer) => {
|
|
1382
|
+
const sub = observable.subscribe(observer);
|
|
1383
|
+
return () => sub?.unsubscribe?.();
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
// Fast implementation for Set & Maps which are generally optimized
|
|
1387
|
+
// by the runtime when using `for..of` loops
|
|
1388
|
+
if (input instanceof Set || input instanceof Map) {
|
|
1389
|
+
const collection = input;
|
|
1390
|
+
const size = collection.size;
|
|
1391
|
+
if (size === 0)
|
|
1392
|
+
return new Constructor(EMPTY);
|
|
1393
|
+
return new Constructor((obs) => {
|
|
1394
|
+
// For...of is optimized for Sets in V8
|
|
1395
|
+
for (const item of collection) {
|
|
1396
|
+
if (throwError)
|
|
1397
|
+
(0, error_js_1.assertObservableError)(item, obs);
|
|
1398
|
+
obs.next(item);
|
|
1399
|
+
if (obs.closed)
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
obs.complete();
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
// Case 2 – promise
|
|
1406
|
+
const promise = input;
|
|
1407
|
+
if (typeof promise.then === "function") {
|
|
1408
|
+
return new Constructor((obs) => {
|
|
1409
|
+
promise.then((value) => {
|
|
1410
|
+
if (throwError)
|
|
1411
|
+
(0, error_js_1.assertObservableError)(value, obs);
|
|
1412
|
+
obs.next(value);
|
|
1413
|
+
obs.complete();
|
|
1414
|
+
},
|
|
1415
|
+
// Error during iteration
|
|
1416
|
+
(err) => obs.error(err));
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
// Case 3 – synchronous iterable
|
|
1420
|
+
const iteratorFn = input[symbol_js_1.Symbol.iterator];
|
|
1421
|
+
if (typeof iteratorFn === "function") {
|
|
1422
|
+
return new Constructor((obs) => {
|
|
1423
|
+
const iterator = iteratorFn.call(input);
|
|
1424
|
+
try {
|
|
1425
|
+
for (let step = iterator.next(); !step.done; step = iterator.next()) {
|
|
1426
|
+
if (throwError)
|
|
1427
|
+
(0, error_js_1.assertObservableError)(step.value);
|
|
1428
|
+
obs.next(step.value);
|
|
1429
|
+
// If subscription was closed during iteration, clean up and exit
|
|
1430
|
+
if (obs.closed)
|
|
1431
|
+
break;
|
|
1432
|
+
}
|
|
1433
|
+
obs.complete();
|
|
1434
|
+
}
|
|
1435
|
+
catch (err) {
|
|
1436
|
+
obs.error(err);
|
|
1437
|
+
}
|
|
1438
|
+
return () => {
|
|
1439
|
+
if (typeof iterator?.return === "function") {
|
|
1440
|
+
try {
|
|
1441
|
+
iterator.return(); // IteratorClose
|
|
1442
|
+
}
|
|
1443
|
+
catch (err) {
|
|
1444
|
+
queueMicrotask(() => {
|
|
1445
|
+
throw err;
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
// Case 4 – async iterable
|
|
1453
|
+
const asyncIteratorFn = input[symbol_js_1.Symbol.asyncIterator];
|
|
1454
|
+
if (typeof asyncIteratorFn === "function") {
|
|
1455
|
+
return new Constructor((obs) => {
|
|
1456
|
+
const asyncIterator = asyncIteratorFn.call(input);
|
|
1457
|
+
// Start consuming the async iterable
|
|
1458
|
+
(async () => {
|
|
1459
|
+
try {
|
|
1460
|
+
for (let step = await asyncIterator.next(); !step.done; step = await asyncIterator.next()) {
|
|
1461
|
+
if (throwError)
|
|
1462
|
+
(0, error_js_1.assertObservableError)(step.value);
|
|
1463
|
+
obs.next(step.value);
|
|
1464
|
+
// If subscription was closed during iteration, clean up and exit
|
|
1465
|
+
if (obs.closed)
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1468
|
+
// Normal completion
|
|
1469
|
+
obs.complete();
|
|
1470
|
+
}
|
|
1471
|
+
catch (err) {
|
|
1472
|
+
// Error during iteration
|
|
1473
|
+
obs.error(err);
|
|
1474
|
+
}
|
|
1475
|
+
})();
|
|
1476
|
+
return () => {
|
|
1477
|
+
if (typeof asyncIterator?.return === "function") {
|
|
1478
|
+
try {
|
|
1479
|
+
asyncIterator.return(); // IteratorClose
|
|
1480
|
+
}
|
|
1481
|
+
catch (err) {
|
|
1482
|
+
queueMicrotask(() => {
|
|
1483
|
+
throw err;
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
throw new TypeError("Input is not Observable, Iterable, AsyncIterable, Promise, or ReadableStream");
|
|
1491
|
+
}
|
|
1492
|
+
async function* pull(observable, { strategy = { highWaterMark: 64 }, throwError = true } = {}) {
|
|
1493
|
+
const obs = observable?.[symbol_js_1.Symbol.observable]?.();
|
|
1494
|
+
let sub = null;
|
|
1495
|
+
// Create a ReadableStream that will buffer values from the Observable
|
|
1496
|
+
const stream = new ReadableStream({
|
|
1497
|
+
start: (ctrl) => {
|
|
1498
|
+
// Subscribe to the Observable and connect it to the stream
|
|
1499
|
+
sub = obs?.subscribe({
|
|
1500
|
+
// Normal values flow directly into the stream
|
|
1501
|
+
next: (v) => ctrl.enqueue(v),
|
|
1502
|
+
// Errors are wrapped as special values rather than using stream.error()
|
|
1503
|
+
// This ensures values emitted before the error are still processed
|
|
1504
|
+
error: (e) => {
|
|
1505
|
+
ctrl.enqueue(error_js_1.ObservableError.from(e, "observable:pull"));
|
|
1506
|
+
sub = null;
|
|
1507
|
+
},
|
|
1508
|
+
// Close the stream when the Observable completes
|
|
1509
|
+
complete: () => {
|
|
1510
|
+
ctrl.close();
|
|
1511
|
+
sub = null;
|
|
1512
|
+
},
|
|
1513
|
+
});
|
|
1514
|
+
},
|
|
1515
|
+
// Clean up the subscription if the stream is cancelled
|
|
1516
|
+
// This happens when the AsyncGenerator is terminated early
|
|
1517
|
+
cancel: () => {
|
|
1518
|
+
sub?.unsubscribe();
|
|
1519
|
+
sub = null;
|
|
1520
|
+
},
|
|
1521
|
+
}, strategy);
|
|
1522
|
+
// Get a reader for the stream and yield values as they become available
|
|
1523
|
+
const reader = stream.getReader();
|
|
1524
|
+
try {
|
|
1525
|
+
while (true) {
|
|
1526
|
+
// Wait for the next value (with backpressure automatically applied)
|
|
1527
|
+
const { value, done } = await reader.read();
|
|
1528
|
+
// If we received a wrapped error, unwrap and throw it
|
|
1529
|
+
if (throwError)
|
|
1530
|
+
(0, error_js_1.assertObservableError)(value);
|
|
1531
|
+
// If the stream is done (Observable completed), exit the loop
|
|
1532
|
+
if (done)
|
|
1533
|
+
break;
|
|
1534
|
+
// Otherwise, yield the value to the consumer
|
|
1535
|
+
yield value;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
finally {
|
|
1539
|
+
// Ensure resources are cleaned up even if iteration is terminated early
|
|
1540
|
+
// This guarantees no memory leaks, even with break or thrown exceptions
|
|
1541
|
+
reader.releaseLock();
|
|
1542
|
+
await stream.cancel();
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Checks if a value is an Observable instance from this library.
|
|
1547
|
+
*
|
|
1548
|
+
* When working with different Observable implementations or mixed data types, you often need
|
|
1549
|
+
* to verify what kind of object you're dealing with. This function provides a reliable way to
|
|
1550
|
+
* check if something is specifically an instance of our Observable class, which is helpful
|
|
1551
|
+
* for type safety and ensuring you can use all the methods available on our implementation.
|
|
1552
|
+
*
|
|
1553
|
+
* **Why This Function Exists**:
|
|
1554
|
+
*
|
|
1555
|
+
* In JavaScript ecosystems, you might encounter different Observable implementations - RxJS,
|
|
1556
|
+
* this library, custom implementations, or objects that just happen to have a `subscribe` method.
|
|
1557
|
+
* Without a proper way to distinguish between them, you'd have to either:
|
|
1558
|
+
* - Risk calling methods that don't exist (crashes your app)
|
|
1559
|
+
* - Write defensive code with lots of property checks (clutters your logic)
|
|
1560
|
+
* - Use duck typing that might give false positives (unreliable)
|
|
1561
|
+
*
|
|
1562
|
+
* This function eliminates those problems by giving you a definitive answer: "Is this an
|
|
1563
|
+
* Observable from our library?" If yes, you know exactly what methods and properties are
|
|
1564
|
+
* available.
|
|
1565
|
+
*
|
|
1566
|
+
* **How It Relates to Other Checks**:
|
|
1567
|
+
*
|
|
1568
|
+
* Think of this as the strict cousin of `isSpecObservable()`. While `isSpecObservable()`
|
|
1569
|
+
* asks "can I subscribe to this?" this function asks "is this specifically our Observable?"
|
|
1570
|
+
*
|
|
1571
|
+
* Use `isObservable()` when you need to ensure you're working with our exact implementation,
|
|
1572
|
+
* and `isSpecObservable()` when you just need something subscribable.
|
|
1573
|
+
*
|
|
1574
|
+
* **Performance Story**:
|
|
1575
|
+
*
|
|
1576
|
+
* This function uses `instanceof`, which modern JavaScript engines optimize very well. It's
|
|
1577
|
+
* essentially a pointer comparison under the hood, making it extremely fast and suitable for
|
|
1578
|
+
* use in performance-critical code paths.
|
|
1579
|
+
*
|
|
1580
|
+
* The performance characteristics are:
|
|
1581
|
+
* - Single `instanceof` check (optimized by JavaScript engines)
|
|
1582
|
+
* - No method calls or property access required
|
|
1583
|
+
* - Safe to use in tight loops or frequently called functions
|
|
1584
|
+
* - Memory efficient (no allocations, just a boolean return)
|
|
1585
|
+
*
|
|
1586
|
+
* **Common Ways to Use This Function**:
|
|
1587
|
+
*
|
|
1588
|
+
* ```typescript
|
|
1589
|
+
* // Scenario 1: Type-safe method access
|
|
1590
|
+
* function processObservable(input: unknown) {
|
|
1591
|
+
* if (isObservable(input)) {
|
|
1592
|
+
* // TypeScript now knows input is Observable<unknown>
|
|
1593
|
+
* const generator = input.pull({ strategy: { highWaterMark: 10 } });
|
|
1594
|
+
* return generator; // Can safely use our specific methods
|
|
1595
|
+
* }
|
|
1596
|
+
*
|
|
1597
|
+
* throw new Error('Expected an Observable from this library');
|
|
1598
|
+
* }
|
|
1599
|
+
*
|
|
1600
|
+
* // Scenario 2: Library interoperability
|
|
1601
|
+
* function convertToOurObservable(source: unknown): Observable<any> {
|
|
1602
|
+
* if (isObservable(source)) {
|
|
1603
|
+
* return source; // Already our type, no conversion needed
|
|
1604
|
+
* }
|
|
1605
|
+
*
|
|
1606
|
+
* if (isSpecObservable(source)) {
|
|
1607
|
+
* return Observable.from(source); // Convert from other implementation
|
|
1608
|
+
* }
|
|
1609
|
+
*
|
|
1610
|
+
* throw new Error('Cannot convert to Observable');
|
|
1611
|
+
* }
|
|
1612
|
+
*
|
|
1613
|
+
* // Scenario 3: Filtering mixed arrays
|
|
1614
|
+
* const mixedSources = [rxjsObservable, ourObservable, promise, array];
|
|
1615
|
+
* const ourObservables = mixedSources.filter(isObservable);
|
|
1616
|
+
* // ourObservables is now Observable[] with full type safety
|
|
1617
|
+
*
|
|
1618
|
+
* // Scenario 4: Defensive programming
|
|
1619
|
+
* function subscribeToSource(source: unknown) {
|
|
1620
|
+
* if (isObservable(source)) {
|
|
1621
|
+
* // We know exactly what methods are available
|
|
1622
|
+
* return source.subscribe({ next: console.log });
|
|
1623
|
+
* } else if (isSpecObservable(source)) {
|
|
1624
|
+
* // Different Observable implementation, but still subscribable
|
|
1625
|
+
* return source.subscribe({ next: console.log });
|
|
1626
|
+
* } else {
|
|
1627
|
+
* throw new Error('Source is not observable');
|
|
1628
|
+
* }
|
|
1629
|
+
* }
|
|
1630
|
+
* ```
|
|
1631
|
+
*
|
|
1632
|
+
* **What Makes This Function Reliable**:
|
|
1633
|
+
*
|
|
1634
|
+
* Unlike duck typing (checking for the presence of methods), this function is precise:
|
|
1635
|
+
* - Returns true only for actual instances of our Observable class
|
|
1636
|
+
* - Handles inheritance correctly (subclasses return true)
|
|
1637
|
+
* - Never gives false positives from look-alike objects
|
|
1638
|
+
* - Works correctly across different module loading scenarios
|
|
1639
|
+
*
|
|
1640
|
+
* **Edge Cases Handled**:
|
|
1641
|
+
* - `null` and `undefined` → false (not Observables)
|
|
1642
|
+
* - Objects with `subscribe` methods → false (unless they're actually our Observable)
|
|
1643
|
+
* - Subclasses of Observable → true (proper inheritance support)
|
|
1644
|
+
* - Cross-frame instances → true (same constructor reference)
|
|
1645
|
+
*
|
|
1646
|
+
* **When to Use This vs Other Options**:
|
|
1647
|
+
*
|
|
1648
|
+
* Choose `isObservable()` when:
|
|
1649
|
+
* - You need to access methods specific to our Observable implementation
|
|
1650
|
+
* - You're building type guards for strict type checking
|
|
1651
|
+
* - You need to distinguish between different Observable libraries
|
|
1652
|
+
* - You're doing performance-critical filtering of mixed object types
|
|
1653
|
+
*
|
|
1654
|
+
* Choose `isSpecObservable()` instead when:
|
|
1655
|
+
* - You just need something that can be subscribed to
|
|
1656
|
+
* - You want maximum compatibility with other Observable implementations
|
|
1657
|
+
* - You're building generic utilities that work with any Observable-like object
|
|
1658
|
+
*
|
|
1659
|
+
* @template T - The expected type for the Observable's emitted values
|
|
1660
|
+
* @param value - Any value that might or might not be our Observable
|
|
1661
|
+
* @returns true if the value is an instance of our Observable class, false otherwise
|
|
1662
|
+
*
|
|
1663
|
+
* @example Simple type checking
|
|
1664
|
+
* ```typescript
|
|
1665
|
+
* const maybeObservable: unknown = getDataSource();
|
|
1666
|
+
*
|
|
1667
|
+
* if (isObservable(maybeObservable)) {
|
|
1668
|
+
* // TypeScript knows maybeObservable is Observable<unknown>
|
|
1669
|
+
* const subscription = maybeObservable.subscribe(console.log);
|
|
1670
|
+
*
|
|
1671
|
+
* // Can also use our specific methods
|
|
1672
|
+
* for await (const value of maybeObservable.pull()) {
|
|
1673
|
+
* console.log('Pulled:', value);
|
|
1674
|
+
* }
|
|
1675
|
+
* } else {
|
|
1676
|
+
* console.log('Not our Observable implementation');
|
|
1677
|
+
* }
|
|
1678
|
+
* ```
|
|
1679
|
+
*
|
|
1680
|
+
* @example Building a conversion utility
|
|
1681
|
+
* ```typescript
|
|
1682
|
+
* function ensureOurObservable<T>(source: unknown): Observable<T> {
|
|
1683
|
+
* if (isObservable<T>(source)) {
|
|
1684
|
+
* return source; // Already the right type
|
|
1685
|
+
* }
|
|
1686
|
+
*
|
|
1687
|
+
* if (isSpecObservable<T>(source)) {
|
|
1688
|
+
* // Convert from another Observable implementation
|
|
1689
|
+
* return new Observable<T>(observer => {
|
|
1690
|
+
* const sub = source.subscribe(observer);
|
|
1691
|
+
* return () => sub.unsubscribe();
|
|
1692
|
+
* });
|
|
1693
|
+
* }
|
|
1694
|
+
*
|
|
1695
|
+
* // Try to convert from other types
|
|
1696
|
+
* return Observable.from(source as any);
|
|
1697
|
+
* }
|
|
1698
|
+
* ```
|
|
1699
|
+
*
|
|
1700
|
+
* @example Library integration
|
|
1701
|
+
* ```typescript
|
|
1702
|
+
* // Function that works with any Observable but optimizes for ours
|
|
1703
|
+
* function processStream<T>(stream: unknown): AsyncGenerator<T> {
|
|
1704
|
+
* if (isObservable<T>(stream)) {
|
|
1705
|
+
* // Use our optimized pull method
|
|
1706
|
+
* return stream.pull();
|
|
1707
|
+
* } else if (isSpecObservable<T>(stream)) {
|
|
1708
|
+
* // Convert and then use our method
|
|
1709
|
+
* return Observable.from(stream).pull();
|
|
1710
|
+
* } else {
|
|
1711
|
+
* throw new Error('Expected an Observable-like object');
|
|
1712
|
+
* }
|
|
1713
|
+
* }
|
|
1714
|
+
* ```
|
|
1715
|
+
*/
|
|
1716
|
+
function isObservable(value) {
|
|
1717
|
+
// This is a straightforward instanceof check
|
|
1718
|
+
// Works reliably across module boundaries and handles inheritance correctly
|
|
1719
|
+
return value instanceof Observable;
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Checks if a value conforms to the Observable specification protocol.
|
|
1723
|
+
*
|
|
1724
|
+
* When building applications that work with multiple Observable implementations, you need a way
|
|
1725
|
+
* to identify objects that can be subscribed to, regardless of which specific library created
|
|
1726
|
+
* them. This function provides that capability by checking for the core Observable protocol
|
|
1727
|
+
* rather than specific implementation details.
|
|
1728
|
+
*
|
|
1729
|
+
* **Why This Function Exists**:
|
|
1730
|
+
*
|
|
1731
|
+
* The Observable ecosystem includes many implementations - RxJS, this library, Zen Observable,
|
|
1732
|
+
* and others. Each has its own class structure, but they all follow the same basic protocol:
|
|
1733
|
+
* having a `[Symbol.observable]()` method that returns an object with a `subscribe()` method.
|
|
1734
|
+
*
|
|
1735
|
+
* Without this function, you'd need to write complex checks to determine if something is
|
|
1736
|
+
* subscribable, leading to:
|
|
1737
|
+
* - Fragile duck typing that breaks with edge cases
|
|
1738
|
+
* - Verbose property checking that clutters your code
|
|
1739
|
+
* - Missing compatibility with new Observable implementations
|
|
1740
|
+
* - Inconsistent behavior across different parts of your application
|
|
1741
|
+
*
|
|
1742
|
+
* This function solves those problems by implementing the official Observable protocol check.
|
|
1743
|
+
*
|
|
1744
|
+
* **How It Relates to Other Checks**:
|
|
1745
|
+
*
|
|
1746
|
+
* Think of this as the diplomatic cousin of `isObservable()`. While `isObservable()` checks
|
|
1747
|
+
* for our specific implementation, this function asks "do you speak the Observable protocol?"
|
|
1748
|
+
* It's designed for interoperability and maximum compatibility.
|
|
1749
|
+
*
|
|
1750
|
+
* The relationship between these functions is:
|
|
1751
|
+
* - `isObservable()` → "Are you our exact Observable class?"
|
|
1752
|
+
* - `isSpecObservable()` → "Can I subscribe to you using the standard protocol?"
|
|
1753
|
+
*
|
|
1754
|
+
* **Performance Story**:
|
|
1755
|
+
*
|
|
1756
|
+
* This function is more complex than `isObservable()` because it needs to check multiple
|
|
1757
|
+
* properties and call a method. However, it's still quite efficient:
|
|
1758
|
+
*
|
|
1759
|
+
* - Fast property access for Symbol.observable
|
|
1760
|
+
* - Single method call to get the subscribable object
|
|
1761
|
+
* - Type checking for the subscribe method
|
|
1762
|
+
* - Early returns for non-objects to avoid unnecessary work
|
|
1763
|
+
*
|
|
1764
|
+
* While not as fast as `instanceof`, it's still suitable for most use cases. If you're in a
|
|
1765
|
+
* performance-critical path and know you're only dealing with our Observable implementation,
|
|
1766
|
+
* prefer `isObservable()`.
|
|
1767
|
+
*
|
|
1768
|
+
* **Common Ways to Use This Function**:
|
|
1769
|
+
*
|
|
1770
|
+
* ```typescript
|
|
1771
|
+
* // Scenario 1: Cross-library compatibility
|
|
1772
|
+
* import { Observable as RxObservable } from 'rxjs';
|
|
1773
|
+
* import { Observable as OurObservable } from './observable.ts';
|
|
1774
|
+
*
|
|
1775
|
+
* function processAnyObservable<T>(source: unknown): Promise<T[]> {
|
|
1776
|
+
* if (isSpecObservable<T>(source)) {
|
|
1777
|
+
* // Works with RxJS, our Observable, or any other spec-compliant implementation
|
|
1778
|
+
* const results: T[] = [];
|
|
1779
|
+
*
|
|
1780
|
+
* return new Promise((resolve, reject) => {
|
|
1781
|
+
* source.subscribe({
|
|
1782
|
+
* next: value => results.push(value),
|
|
1783
|
+
* error: reject,
|
|
1784
|
+
* complete: () => resolve(results)
|
|
1785
|
+
* });
|
|
1786
|
+
* });
|
|
1787
|
+
* }
|
|
1788
|
+
*
|
|
1789
|
+
* throw new Error('Source must be Observable-like');
|
|
1790
|
+
* }
|
|
1791
|
+
*
|
|
1792
|
+
* // Scenario 2: Building generic utilities
|
|
1793
|
+
* function toArray<T>(source: unknown): Promise<T[]> {
|
|
1794
|
+
* if (isSpecObservable<T>(source)) {
|
|
1795
|
+
* return new Promise((resolve, reject) => {
|
|
1796
|
+
* const items: T[] = [];
|
|
1797
|
+
* source.subscribe({
|
|
1798
|
+
* next: item => items.push(item),
|
|
1799
|
+
* error: reject,
|
|
1800
|
+
* complete: () => resolve(items)
|
|
1801
|
+
* });
|
|
1802
|
+
* });
|
|
1803
|
+
* }
|
|
1804
|
+
*
|
|
1805
|
+
* // Fallback for other iterable types
|
|
1806
|
+
* if (Array.isArray(source)) return Promise.resolve([...source]);
|
|
1807
|
+
*
|
|
1808
|
+
* throw new Error('Cannot convert to array');
|
|
1809
|
+
* }
|
|
1810
|
+
*
|
|
1811
|
+
* // Scenario 3: Input validation in APIs
|
|
1812
|
+
* function subscribeToStream<T>(
|
|
1813
|
+
* stream: unknown,
|
|
1814
|
+
* handler: (value: T) => void
|
|
1815
|
+
* ): () => void {
|
|
1816
|
+
* if (!isSpecObservable<T>(stream)) {
|
|
1817
|
+
* throw new TypeError('Expected an Observable-like object');
|
|
1818
|
+
* }
|
|
1819
|
+
*
|
|
1820
|
+
* const subscription = stream.subscribe({ next: handler });
|
|
1821
|
+
* return () => subscription.unsubscribe();
|
|
1822
|
+
* }
|
|
1823
|
+
*
|
|
1824
|
+
* // Scenario 4: Filtering and type narrowing
|
|
1825
|
+
* const mixedSources: unknown[] = [
|
|
1826
|
+
* rxjsObservable,
|
|
1827
|
+
* ourObservable,
|
|
1828
|
+
* { subscribe() { return { unsubscribe() {} }; } }, // Custom implementation
|
|
1829
|
+
* "not observable",
|
|
1830
|
+
* 42
|
|
1831
|
+
* ];
|
|
1832
|
+
*
|
|
1833
|
+
* const observableSources = mixedSources.filter(isSpecObservable);
|
|
1834
|
+
* // observableSources is now Array<{ subscribe: Function, [Symbol.observable]: Function }>
|
|
1835
|
+
* ```
|
|
1836
|
+
*
|
|
1837
|
+
* **What Makes This Function Robust**:
|
|
1838
|
+
*
|
|
1839
|
+
* This function implements the official Observable protocol checking:
|
|
1840
|
+
* 1. Verifies the object has `Symbol.observable` method
|
|
1841
|
+
* 2. Calls that method to get the subscribable object
|
|
1842
|
+
* 3. Ensures the result has a working `subscribe` method
|
|
1843
|
+
* 4. Handles errors gracefully (returns false rather than throwing)
|
|
1844
|
+
*
|
|
1845
|
+
* **Edge Cases Handled**:
|
|
1846
|
+
* - `null` and `undefined` → false (not objects)
|
|
1847
|
+
* - Objects without `Symbol.observable` → false (not Observable protocol)
|
|
1848
|
+
* - `Symbol.observable` that throws → false (graceful error handling)
|
|
1849
|
+
* - `Symbol.observable` returning non-objects → false (invalid protocol)
|
|
1850
|
+
* - Objects with `subscribe` but no `Symbol.observable` → false (incomplete protocol)
|
|
1851
|
+
*
|
|
1852
|
+
* **When to Use This vs Other Options**:
|
|
1853
|
+
*
|
|
1854
|
+
* Choose `isSpecObservable()` when:
|
|
1855
|
+
* - Building libraries that should work with any Observable implementation
|
|
1856
|
+
* - You need maximum compatibility across the Observable ecosystem
|
|
1857
|
+
* - You're creating utilities for consuming streams regardless of their origin
|
|
1858
|
+
* - You want to follow the official Observable specification strictly
|
|
1859
|
+
*
|
|
1860
|
+
* Choose `isObservable()` instead when:
|
|
1861
|
+
* - You need methods specific to our Observable implementation
|
|
1862
|
+
* - Performance is critical and you know the expected types
|
|
1863
|
+
* - You're working within a single Observable implementation ecosystem
|
|
1864
|
+
* - You need compile-time guarantees about available methods
|
|
1865
|
+
*
|
|
1866
|
+
* @template T - The expected type for values emitted by the Observable
|
|
1867
|
+
* @param value - Any value that might conform to the Observable protocol
|
|
1868
|
+
* @returns true if the value implements the Observable specification, false otherwise
|
|
1869
|
+
*
|
|
1870
|
+
* @example Cross-library compatibility
|
|
1871
|
+
* ```typescript
|
|
1872
|
+
* import { Observable as RxObservable } from 'rxjs';
|
|
1873
|
+
* import { Observable as OurObservable } from './observable.ts';
|
|
1874
|
+
*
|
|
1875
|
+
* const sources = [
|
|
1876
|
+
* new RxObservable(sub => sub.next(1)),
|
|
1877
|
+
* new OurObservable(obs => obs.next(2)),
|
|
1878
|
+
* { subscribe() { return { unsubscribe() {} }; } } // Custom
|
|
1879
|
+
* ];
|
|
1880
|
+
*
|
|
1881
|
+
* // Process any Observable-like object
|
|
1882
|
+
* sources.forEach(source => {
|
|
1883
|
+
* if (isSpecObservable(source)) {
|
|
1884
|
+
* console.log('Can subscribe to this source');
|
|
1885
|
+
* source.subscribe({ next: console.log });
|
|
1886
|
+
* }
|
|
1887
|
+
* });
|
|
1888
|
+
* ```
|
|
1889
|
+
*
|
|
1890
|
+
* @example Building a universal Observable utility
|
|
1891
|
+
* ```typescript
|
|
1892
|
+
* function first<T>(source: unknown): Promise<T> {
|
|
1893
|
+
* if (!isSpecObservable<T>(source)) {
|
|
1894
|
+
* return Promise.reject(new Error('Source must be Observable'));
|
|
1895
|
+
* }
|
|
1896
|
+
*
|
|
1897
|
+
* return new Promise((resolve, reject) => {
|
|
1898
|
+
* const subscription = source.subscribe({
|
|
1899
|
+
* next: value => {
|
|
1900
|
+
* subscription.unsubscribe();
|
|
1901
|
+
* resolve(value);
|
|
1902
|
+
* },
|
|
1903
|
+
* error: reject,
|
|
1904
|
+
* complete: () => reject(new Error('Observable completed without emitting'))
|
|
1905
|
+
* });
|
|
1906
|
+
* });
|
|
1907
|
+
* }
|
|
1908
|
+
*
|
|
1909
|
+
* // Works with any Observable implementation
|
|
1910
|
+
* const result1 = await first(rxjsObservable);
|
|
1911
|
+
* const result2 = await first(ourObservable);
|
|
1912
|
+
* ```
|
|
1913
|
+
*
|
|
1914
|
+
* @example Input validation for APIs
|
|
1915
|
+
* ```typescript
|
|
1916
|
+
* interface StreamProcessor<T> {
|
|
1917
|
+
* process(stream: unknown): AsyncGenerator<T>;
|
|
1918
|
+
* }
|
|
1919
|
+
*
|
|
1920
|
+
* class UniversalProcessor<T> implements StreamProcessor<T> {
|
|
1921
|
+
* async* process(stream: unknown): AsyncGenerator<T> {
|
|
1922
|
+
* if (isSpecObservable<T>(stream)) {
|
|
1923
|
+
* // Convert any Observable to async generator
|
|
1924
|
+
* const observable = stream[Symbol.observable]();
|
|
1925
|
+
*
|
|
1926
|
+
* let resolve: (value: IteratorResult<T>) => void;
|
|
1927
|
+
* let reject: (error: any) => void;
|
|
1928
|
+
* let promise = new Promise<IteratorResult<T>>((res, rej) => {
|
|
1929
|
+
* resolve = res;
|
|
1930
|
+
* reject = rej;
|
|
1931
|
+
* });
|
|
1932
|
+
*
|
|
1933
|
+
* const subscription = observable.subscribe({
|
|
1934
|
+
* next: value => {
|
|
1935
|
+
* resolve({ value, done: false });
|
|
1936
|
+
* promise = new Promise<IteratorResult<T>>((res, rej) => {
|
|
1937
|
+
* resolve = res;
|
|
1938
|
+
* reject = rej;
|
|
1939
|
+
* });
|
|
1940
|
+
* },
|
|
1941
|
+
* error: reject,
|
|
1942
|
+
* complete: () => resolve({ value: undefined as any, done: true })
|
|
1943
|
+
* });
|
|
1944
|
+
*
|
|
1945
|
+
* try {
|
|
1946
|
+
* while (true) {
|
|
1947
|
+
* const result = await promise;
|
|
1948
|
+
* if (result.done) break;
|
|
1949
|
+
* yield result.value;
|
|
1950
|
+
* }
|
|
1951
|
+
* } finally {
|
|
1952
|
+
* subscription.unsubscribe();
|
|
1953
|
+
* }
|
|
1954
|
+
* } else {
|
|
1955
|
+
* throw new Error('Input must implement Observable protocol');
|
|
1956
|
+
* }
|
|
1957
|
+
* }
|
|
1958
|
+
* }
|
|
1959
|
+
* ```
|
|
1960
|
+
*/
|
|
1961
|
+
function isSpecObservable(value) {
|
|
1962
|
+
// Early return for non-objects
|
|
1963
|
+
if (value === null || value === undefined || typeof value !== "object") {
|
|
1964
|
+
return false;
|
|
1965
|
+
}
|
|
1966
|
+
try {
|
|
1967
|
+
// Check if the object has the Symbol.observable method
|
|
1968
|
+
const observableMethod = value[symbol_js_1.Symbol.observable];
|
|
1969
|
+
if (typeof observableMethod !== "function") {
|
|
1970
|
+
return false;
|
|
1971
|
+
}
|
|
1972
|
+
// Call the method to get the subscribable object
|
|
1973
|
+
const subscribable = observableMethod.call(value);
|
|
1974
|
+
// Verify the result has a subscribe method
|
|
1975
|
+
return (subscribable !== null &&
|
|
1976
|
+
subscribable !== undefined &&
|
|
1977
|
+
typeof subscribable === "object" &&
|
|
1978
|
+
typeof subscribable.subscribe === "function");
|
|
1979
|
+
}
|
|
1980
|
+
catch {
|
|
1981
|
+
// If any step throws, it's not a valid Observable
|
|
1982
|
+
return false;
|
|
1983
|
+
}
|
|
1984
|
+
}
|