├── .gitignore
├── .pr-preview.json
├── .prettierrc
├── w3c.json
├── LICENSE.md
├── .github
└── workflows
│ └── build.yml
├── CONTRIBUTING.md
├── security-privacy-questionnaire.md
├── README.md
└── spec.bs
/.gitignore:
--------------------------------------------------------------------------------
1 | spec.html
2 |
--------------------------------------------------------------------------------
/.pr-preview.json:
--------------------------------------------------------------------------------
1 | {
2 | "src_file": "spec.bs",
3 | "type": "bikeshed"
4 | }
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "singleQuote": true,
4 | "trailingComma": "all"
5 | }
--------------------------------------------------------------------------------
/w3c.json:
--------------------------------------------------------------------------------
1 | {
2 | "group": [80485]
3 | , "contacts": ["cwilso"]
4 | , "repo-type": "cg-report"
5 | }
6 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | All Reports in this Repository are licensed by Contributors
2 | under the
3 | [W3C Software and Document License](http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document).
4 |
5 | Contributions to Specifications are made under the
6 | [W3C CLA](https://www.w3.org/community/about/agreements/cla/).
7 |
8 | Contributions to Test Suites are made under the
9 | [W3C 3-clause BSD License](https://www.w3.org/Consortium/Legal/2008/03-bsd-license.html)
10 |
11 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 | on:
3 | pull_request: {}
4 | push:
5 | branches:
6 | - master
7 | jobs:
8 | build:
9 | name: Build
10 | runs-on: ubuntu-latest
11 | permissions:
12 | contents: write
13 | steps:
14 | - uses: actions/checkout@v3
15 | - uses: w3c/spec-prod@v2
16 | with:
17 | TOOLCHAIN: bikeshed
18 | SOURCE: spec.bs
19 | DESTINATION: index.html
20 | GH_PAGES_BRANCH: gh-pages
21 | BUILD_FAIL_ON: warning
22 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Web Platform Incubator Community Group
2 |
3 | This repository is being used for work in the W3C Web Platform Incubator Community Group, governed by the [W3C Community License
4 | Agreement (CLA)](http://www.w3.org/community/about/agreements/cla/). To make substantive contributions,
5 | you must join the CG.
6 |
7 | If you are not the sole contributor to a contribution (pull request), please identify all
8 | contributors in the pull request comment.
9 |
10 | To add a contributor (other than yourself, that's automatic), mark them one per line as follows:
11 |
12 | ```
13 | +@github_username
14 | ```
15 |
16 | If you added a contributor by mistake, you can remove them in a comment with:
17 |
18 | ```
19 | -@github_username
20 | ```
21 |
22 | If you are making a pull request on behalf of someone else but you had no part in designing the
23 | feature, you can remove yourself with the above syntax.
24 |
--------------------------------------------------------------------------------
/security-privacy-questionnaire.md:
--------------------------------------------------------------------------------
1 | # Observable API: Security and Privacy Questionnaire Answers
2 |
3 | The following are the answers to the W3C TAG's [security and privacy self-review
4 | questionnaire](https://w3ctag.github.io/security-questionnaire/).
5 |
6 | **What information does this feature expose, and for what purposes?**
7 |
8 | This API exposes the same event handling information as the web's existing
9 | native event handling mechanism, just in a more ergonomic way.
10 |
11 | **Do features in your specification expose the minimum amount of information necessary to implement the intended functionality?**
12 |
13 | Yes.
14 |
15 | **Do the features in your specification expose personal information, personally-identifiable information (PII), or information derived from either?**
16 |
17 | No.
18 |
19 | **How do the features in your specification deal with sensitive information?**
20 |
21 | There isn't really sensitive information directly involved with this API. In
22 | this area, its properties are the same as the web's native event handling mechanism.
23 |
24 | **Do the features in your specification introduce state that persists across browsing sessions?**
25 |
26 | No.
27 |
28 | **Do the features in your specification expose information about the underlying platform to origins?**
29 |
30 | No.
31 |
32 | **Does this specification allow an origin to send data to the underlying platform?**
33 |
34 | No.
35 |
36 | **Do features in this specification enable access to device sensors?**
37 |
38 | No.
39 |
40 | **Do features in this specification enable new script execution/loading mechanisms?**
41 |
42 | No.
43 |
44 | **Do features in this specification allow an origin to access other devices?**
45 |
46 | No.
47 |
48 | **Do features in this specification allow an origin some measure of control over a user agent's native UI?**
49 |
50 | No.
51 |
52 | **What temporary identifiers do the features in this specification create or expose to the web?**
53 |
54 | No identifiers.
55 |
56 | **How does this specification distinguish between behavior in first-party and third-party contexts?**
57 |
58 | It doesn't. Specifically, event handling bubbles/captures in the same way as it
59 | does before: *not* crossing the frame boundary.
60 |
61 | **How do the features in this specification work in the context of a browser’s Private Browsing or Incognito mode?**
62 |
63 | This proposal prescribes *no* difference. Any difference that there might be
64 | would be a result of the browser's intervening on behalf of certain trusted,
65 | script-exposed events, but we are not aware of any examples of this, and these
66 | are already relevant to today's existing event handling mechanisms and exist
67 | outside of this API's purview.
68 |
69 | **Does this specification have both "Security Considerations" and "Privacy Considerations" sections?**
70 |
71 | No, there is no specification just yet.
72 |
73 | **Do features in your specification enable origins to downgrade default security protections?**
74 |
75 | No.
76 |
77 | **What happens when a document that uses your feature is kept alive in BFCache (instead of getting destroyed) after navigation, and potentially gets reused on future navigations back to the document?**
78 |
79 | The same exact thing that happens with the web's native event handling
80 | mechanisms, which this API largely piggy-backs off of. Assuming events are
81 | "paused" in those contexts (either by pausing script execution or making it
82 | impossible for users to trigger script-observable events), then an Observable's
83 | handlers would not be called, because the stream of events would be inactive for
84 | a period of time. But otherwise, this API does not prescribe anything specific.
85 |
86 | **What happens when a document that uses your feature gets disconnected?**
87 |
88 | Nothing specific to this API. Any event sources that are stopped, because script
89 | execution is stopped, would result in an Observable whose handlers stop getting
90 | fired.
91 |
92 | **What should this questionnaire have asked?**
93 |
94 | N/A.
95 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Observable
2 |
3 | This is the explainer for the Observable API proposal for more ergonomic and
4 | composable event handling.
5 |
6 | ## Introduction
7 |
8 | ### `EventTarget` integration
9 |
10 | This proposal adds a `.when()` method to `EventTarget` that becomes a better
11 | `addEventListener()`; specifically it returns a [new
12 | `Observable`](#the-observable-api) that adds a new event listener to the target
13 | when its `subscribe()` method is called. The Observable calls the subscriber's
14 | `next()` handler with each event.
15 |
16 | Observables turn event handling, filtering, and termination, into an explicit, declarative flow
17 | that's easier to understand and
18 | [compose](https://stackoverflow.com/questions/44112364/what-does-this-mean-in-the-observable-tc-39-proposal)
19 | than today's imperative version, which often requires nested calls to `addEventListener()` and
20 | hard-to-follow callback chains.
21 |
22 | #### Example 1
23 |
24 | ```js
25 | // Filtering and mapping:
26 | element
27 | .when('click')
28 | .filter((e) => e.target.matches('.foo'))
29 | .map((e) => ({ x: e.clientX, y: e.clientY }))
30 | .subscribe({ next: handleClickAtPoint });
31 | ```
32 |
33 | #### Example 2
34 |
35 | ```js
36 | // Automatic, declarative unsubscription via the takeUntil method:
37 | element.when('mousemove')
38 | .takeUntil(document.when('mouseup'))
39 | .subscribe({next: e => … });
40 |
41 | // Since reduce and some other terminators return promises, they also play
42 | // well with async functions:
43 | await element.when('mousemove')
44 | .takeUntil(element.when('mouseup'))
45 | .reduce((soFar, e) => …);
46 | ```
47 |
48 |
49 | Imperative version
50 |
51 | ```js
52 | // Imperative
53 | const controller = new AbortController();
54 | element.addEventListener('mousemove', e => {
55 | console.log(e);
56 |
57 | element.addEventListener('mouseup', e => {
58 | controller.abort();
59 | });
60 | }, { signal: controller.signal });
61 | ```
62 |
63 |
64 |
65 | #### Example 3
66 |
67 | Tracking all link clicks within a container
68 | ([example](https://github.com/whatwg/dom/issues/544#issuecomment-351705380)):
69 |
70 | ```js
71 | container
72 | .when('click')
73 | .filter((e) => e.target.closest('a'))
74 | .subscribe({
75 | next: (e) => {
76 | // …
77 | },
78 | });
79 | ```
80 |
81 | #### Example 4
82 |
83 | Find the maximum Y coordinate while the mouse is held down
84 | ([example](https://github.com/whatwg/dom/issues/544#issuecomment-351762493)):
85 |
86 | ```js
87 | const maxY = await element
88 | .when('mousemove')
89 | .takeUntil(element.when('mouseup'))
90 | .map((e) => e.clientY)
91 | .reduce((soFar, y) => Math.max(soFar, y), 0);
92 | ```
93 |
94 | #### Example 5
95 |
96 | Multiplexing a `WebSocket`, such that a subscription message is send on connection,
97 | and an unsubscription message is send to the server when the user unsubscribes.
98 |
99 | ```js
100 | const socket = new WebSocket('wss://example.com');
101 |
102 | function multiplex({ startMsg, stopMsg, match }) {
103 | if (socket.readyState !== WebSocket.OPEN) {
104 | return socket
105 | .when('open')
106 | .flatMap(() => multiplex({ startMsg, stopMsg, match }));
107 | } else {
108 | socket.send(JSON.stringify(startMsg));
109 | return socket
110 | .when('message')
111 | .filter(match)
112 | .takeUntil(socket.when('close'))
113 | .takeUntil(socket.when('error'))
114 | .map((e) => JSON.parse(e.data))
115 | .finally(() => {
116 | socket.send(JSON.stringify(stopMsg));
117 | });
118 | }
119 | }
120 |
121 | function streamStock(ticker) {
122 | return multiplex({
123 | startMsg: { ticker, type: 'sub' },
124 | stopMsg: { ticker, type: 'unsub' },
125 | match: (data) => data.ticker === ticker,
126 | });
127 | }
128 |
129 | const googTrades = streamStock('GOOG');
130 | const nflxTrades = streamStock('NFLX');
131 |
132 | const googController = new AbortController();
133 | googTrades.subscribe({next: updateView}, {signal: googController.signal});
134 | nflxTrades.subscribe({next: updateView, ...});
135 |
136 | // And the stream can disconnect later, which
137 | // automatically sends the unsubscription message
138 | // to the server.
139 | googController.abort();
140 | ```
141 |
142 |
143 | Imperative version
144 |
145 | ```js
146 | // Imperative
147 | function multiplex({ startMsg, stopMsg, match }) {
148 | const start = (callback) => {
149 | const teardowns = [];
150 |
151 | if (socket.readyState !== WebSocket.OPEN) {
152 | const openHandler = () => start({ startMsg, stopMsg, match })(callback);
153 | socket.addEventListener('open', openHandler);
154 | teardowns.push(() => {
155 | socket.removeEventListener('open', openHandler);
156 | });
157 | } else {
158 | socket.send(JSON.stringify(startMsg));
159 | const messageHandler = (e) => {
160 | const data = JSON.parse(e.data);
161 | if (match(data)) {
162 | callback(data);
163 | }
164 | };
165 | socket.addEventListener('message', messageHandler);
166 | teardowns.push(() => {
167 | socket.send(JSON.stringify(stopMsg));
168 | socket.removeEventListener('message', messageHandler);
169 | });
170 | }
171 |
172 | const finalize = () => {
173 | teardowns.forEach((t) => t());
174 | };
175 |
176 | socket.addEventListener('close', finalize);
177 | teardowns.push(() => socket.removeEventListener('close', finalize));
178 | socket.addEventListener('error', finalize);
179 | teardowns.push(() => socket.removeEventListener('error', finalize));
180 |
181 | return finalize;
182 | };
183 |
184 | return start;
185 | }
186 |
187 | function streamStock(ticker) {
188 | return multiplex({
189 | startMsg: { ticker, type: 'sub' },
190 | stopMsg: { ticker, type: 'unsub' },
191 | match: (data) => data.ticker === ticker,
192 | });
193 | }
194 |
195 | const googTrades = streamStock('GOOG');
196 | const nflxTrades = streamStock('NFLX');
197 |
198 | const unsubGoogTrades = googTrades(updateView);
199 | const unsubNflxTrades = nflxTrades(updateView);
200 |
201 | // And the stream can disconnect later, which
202 | // automatically sends the unsubscription message
203 | // to the server.
204 | unsubGoogTrades();
205 | ```
206 |
207 |
208 |
209 | #### Example 6
210 |
211 | Here we're leveraging observables to match a secret code, which is a pattern of
212 | keys the user might hit while using an app:
213 |
214 | ```js
215 | const pattern = [
216 | 'ArrowUp',
217 | 'ArrowUp',
218 | 'ArrowDown',
219 | 'ArrowDown',
220 | 'ArrowLeft',
221 | 'ArrowRight',
222 | 'ArrowLeft',
223 | 'ArrowRight',
224 | 'b',
225 | 'a',
226 | 'Enter',
227 | ];
228 |
229 | const keys = document.when('keydown').map(e => e.key);
230 |
231 | keys
232 | .flatMap(firstKey => {
233 | if (firstKey === pattern[0]) {
234 | return keys
235 | .take(pattern.length - 1)
236 | .every((k, i) => k === pattern[i + 1]);
237 | }
238 | })
239 | .filter(matched => matched)
240 | .subscribe(() => console.log('Secret code matched!'));
241 | ```
242 |
243 |
244 | Imperative version
245 |
246 | ```js
247 | const pattern = [...];
248 |
249 | // Imperative
250 | document.addEventListener('keydown', e => {
251 | const key = e.key;
252 | if (key === pattern[0]) {
253 | let i = 1;
254 | const handler = (e) => {
255 | const nextKey = e.key;
256 | if (nextKey !== pattern[i++]) {
257 | document.removeEventListener('keydown', handler)
258 | } else if (pattern.length === i) {
259 | console.log('Secret code matched!');
260 | document.removeEventListener('keydown', handler)
261 | }
262 | };
263 |
264 | document.addEventListener('keydown', handler);
265 | }
266 | }, {once: true});
267 | ```
268 |
269 |
270 |
271 | ### The `Observable` API
272 |
273 | Observables are first-class objects representing composable, repeated events.
274 | They're like Promises but for multiple events, and specifically with
275 | [`EventTarget` integration](#eventtarget-integration), they are to events what
276 | Promises are to callbacks. They can be:
277 |
278 | - Created by script or by platform APIs, and passed to anyone interested in
279 | consuming events via `subscribe()`
280 | - Fed to [operators](#operators) like `Observable.map()`, to be composed &
281 | transformed without a web of nested callbacks
282 |
283 | Better yet, the transition from event handlers ➡️ Observables is simpler than
284 | that of callbacks ➡️ Promises, since Observables integrate nicely on top of
285 | `EventTarget`, the de facto way of subscribing to events from the platform [and
286 | custom script](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/EventTarget#examples).
287 | As a result, developers can use Observables without migrating tons of code on
288 | the platform, since it's an easy drop-in wherever you're handling events today.
289 |
290 | The proposed API shape can be found in
291 | https://wicg.github.io/observable/#core-infrastructure.
292 |
293 | The creator of an Observable passes in a callback that gets invoked
294 | synchronously whenever `subscribe()` is called. The `subscribe()` method can be
295 | called _any number of times_, and the callback it invokes sets up a new
296 | "subscription" by registering the caller of `subscribe()` as a Observer. With
297 | this in place, the Observable can signal any number of events to the Observer
298 | via the `next()` callback, optionally followed by a single call to either
299 | `complete()` or `error()`, signaling that the stream of data is finished.
300 |
301 | ```js
302 | const observable = new Observable((subscriber) => {
303 | let i = 0;
304 | setInterval(() => {
305 | if (i >= 10) subscriber.complete();
306 | else subscriber.next(i++);
307 | }, 2000);
308 | });
309 |
310 | observable.subscribe({
311 | // Print each value the Observable produces.
312 | next: console.log,
313 | });
314 | ```
315 |
316 | While custom Observables can be useful on their own, the primary use case they
317 | unlock is with event handling. Observables returned by the new
318 | `EventTarget#when()` method are created natively with an internal callback that
319 | uses the same [underlying
320 | mechanism](https://dom.spec.whatwg.org/#add-an-event-listener) as
321 | `addEventListener()`. Therefore calling `subscribe()` essentially registers a
322 | new event listener whose events are exposed through the Observer handler
323 | functions and are composable with the various
324 | [combinators](#operators) available to all Observables.
325 |
326 | #### Constructing & converting objects to Observables
327 |
328 | Observables can be created by their native constructor, as demonstrated above,
329 | or by the `Observable.from()` static method. This method constructs a native
330 | Observable from objects that are any of the following, _in this order_:
331 |
332 | - `Observable` (in which case it just returns the given object)
333 | - `AsyncIterable` (anything with `Symbol.asyncIterator`)
334 | - `Iterable` (anything with `Symbol.iterator`)
335 | - `Promise` (or any thenable)
336 |
337 | Furthermore, any method on the platform that wishes to accept an Observable as a
338 | Web IDL argument, or return one from a callback whose return type is
339 | `Observable` can do so with any of the above objects as well, that get
340 | automatically converted to an Observable. We can accomplish this in one of two
341 | ways that we'll finalize in the Observable specification:
342 |
343 | 1. By making the `Observable` type a special Web IDL type that performs this
344 | ECMAScript Object ➡️ Web IDL conversion automatically, like Web IDL does for
345 | other types.
346 | 2. Require methods and callbacks that work with Observables to specify the type
347 | `any`, and have the corresponding spec prose immediately invoke a conversion
348 | algorithm that the Observable specification will supply. This is similar to
349 | what the Streams Standard [does with async iterables
350 | today](https://streams.spec.whatwg.org/#rs-from).
351 |
352 | The conversation in https://github.com/domfarolino/observable/pull/60 leans
353 | towards option (1).
354 |
355 | #### Lazy, synchronous delivery
356 |
357 | Crucially, Observables are "lazy" in that they do not start emitting data until
358 | they are subscribed to, nor do they queue any data _before_ subscription. They
359 | can also start emitting data synchronously during subscription, unlike Promises
360 | which always queue microtasks when invoking `.then()` handlers. Consider this
361 | [example](https://github.com/whatwg/dom/issues/544#issuecomment-351758385):
362 |
363 | ```js
364 | el.when('click').subscribe({next: () => console.log('One')});
365 | el.when('click').find(() => {…}).then(() => console.log('Three'));
366 | el.click();
367 | console.log('Two');
368 | // Logs "One" "Two" "Three"
369 | ```
370 |
371 | #### Firehose of synchronous data
372 |
373 | By using `AbortController`, you can unsubscribe from an Observable even as it
374 | synchronously emits data _during_ subscription:
375 |
376 | ```js
377 | // An observable that synchronously emits unlimited data during subscription.
378 | let observable = new Observable((subscriber) => {
379 | let i = 0;
380 | while (true) {
381 | subscriber.next(i++);
382 | }
383 | });
384 |
385 | let controller = new AbortController();
386 | observable.subscribe({
387 | next: (data) => {
388 | if (data > 100) controller.abort();
389 | }
390 | }, {signal: controller.signal});
391 | ```
392 |
393 | #### Teardown
394 |
395 | It is critical for an Observable subscriber to be able to register an arbitrary
396 | teardown callback to clean up any resources relevant to the subscription. The
397 | teardown can be registered from within the subscription callback passed into the
398 | `Observable` constructor. When run (upon subscribing), the subscription callback
399 | can register a teardown function via `subscriber.addTeardown()`.
400 |
401 | If the subscriber has already been aborted (i.e., `subscriber.signal.aborted` is
402 | `true`), then the given teardown callback is invoked immediately from within
403 | `addTeardown()`. Otherwise, it is invoked synchronously:
404 |
405 | - From `complete()`, after the subscriber's complete handler (if any) is
406 | invoked
407 | - From `error()`, after the subscriber's error handler (if any) is invoked
408 | - The signal passed to the subscription is aborted by the user.
409 |
410 | ### Operators
411 |
412 | We propose the following operators in addition to the `Observable` interface:
413 |
414 | - `catch()`
415 | - Like `Promise#catch()`, it takes a callback which gets fired after the source
416 | observable errors. It will then map to a new observable, returned by the callback,
417 | unless the error is rethrown.
418 | - `takeUntil(Observable)`
419 | - Returns an observable that mirrors the one that this method is called on,
420 | until the input observable emits its first value
421 | - `finally()`
422 | - Like `Promise.finally()`, it takes a callback which gets fired after the
423 | observable completes in any way (`complete()`/`error()`).
424 | - Returns an `Observable` that mirrors the source observable exactly. The callback
425 | passed to `finally` is fired when a subscription to the resulting observable is terminated
426 | for _any reason_. Either immediately after the source completes or errors, or when the consumer
427 | unsubscribes by aborting the subscription.
428 |
429 | Versions of the above are often present in userland implementations of
430 | observables as they are useful for observable-specific reasons, but in addition
431 | to these we offer a set of common operators that follow existing platform
432 | precedent and can greatly increase utility and adoption. These exist on other
433 | iterables, and are derived from TC39's [iterator helpers
434 | proposal](https://github.com/tc39/proposal-iterator-helpers) which adds the
435 | [following
436 | methods](https://tc39.es/proposal-iterator-helpers/#sec-iteratorprototype) to
437 | `Iterator.prototype`:
438 |
439 | - `map()`
440 | - `filter()`
441 | - `take()`
442 | - `drop()`
443 | - `flatMap()`
444 | - `reduce()`
445 | - `toArray()`
446 | - `forEach()`
447 | - `some()`
448 | - `every()`
449 | - `find()`
450 |
451 | And the following method statically on the `Iterator` constructor:
452 |
453 | - `from()`
454 |
455 | We expect userland libraries to provide more niche operators that integrate with
456 | the `Observable` API central to this proposal, potentially shipping natively if
457 | they get enough momentum to graduate to the platform. But for this initial
458 | proposal, we'd like to restrict the set of operators to those that follow the
459 | precedent stated above, similar to how web platform APIs that are declared
460 | [Setlike](https://webidl.spec.whatwg.org/#es-setlike) and
461 | [Maplike](https://webidl.spec.whatwg.org/#es-maplike) have native properties
462 | inspired by TC39's
463 | [Map](https://tc39.es/ecma262/#sec-properties-of-the-map-prototype-object) and
464 | [Set](https://tc39.es/ecma262/#sec-properties-of-the-set-prototype-object)
465 | objects. Therefore we'd consider most discussion of expanding this set as
466 | out-of-scope for the _initial_ proposal, suitable for discussion in an appendix.
467 | Any long tail of operators could _conceivably_ follow along if there is support
468 | for the native Observable API presented in this explainer.
469 |
470 | Note that the operators `every()`, `find()`, `some()`, and `reduce()` return
471 | Promises whose scheduling differs from that of Observables, which sometimes
472 | means event handlers that call `e.preventDefault()` will run too late. See the
473 | [Concerns](#concerns) section which goes into more detail.
474 |
475 | ## Background & landscape
476 |
477 | To illustrate how Observables fit into the current landscape of other reactive
478 | primitives, see the below table which is an attempt at combining
479 | [two](https://github.com/kriskowal/gtor#a-general-theory-of-reactivity)
480 | other [tables](https://rxjs.dev/guide/observable) that classify reactive
481 | primitives by their interaction with producers & consumers:
482 |
483 |
484 |
485 |
486 | |
487 | Singular |
488 | Plural |
489 |
490 |
491 | |
492 | | Spatial |
493 | Temporal |
494 | Spatial |
495 | Temporal |
496 |
497 |
498 |
499 |
500 | | Push |
501 | Value |
502 | Promise |
503 | Observable |
504 |
505 |
506 | | Pull |
507 | Function |
508 | Async iterator |
509 | Iterable |
510 | Async iterator |
511 |
512 |
513 |
514 |
515 | ### History
516 |
517 | Observables were first proposed to the platform in [TC39](https://github.com/tc39/proposal-observable)
518 | in May of 2015. The proposal failed to gain traction, in part due to some opposition that
519 | the API was suitable to be a language-level primitive. In an attempt to renew the proposal
520 | at a higher level of abstraction, a WHATWG [DOM issue](https://github.com/whatwg/dom/issues/544) was
521 | filed in December of 2017. Despite ample [developer demand](https://foolip.github.io/spec-reactions/),
522 | _lots_ of discussion, and no strong objectors, the DOM Observables proposal sat mostly still for several
523 | years (with some flux in the API design) due to a lack of implementer prioritization.
524 |
525 | Later in 2019, [an attempt](https://github.com/tc39/proposal-observable/issues/201) at reviving the
526 | proposal was made back at the original TC39 repository, which involved some API simplifications and
527 | added support for the synchronous "firehose" problem.
528 |
529 | This repository is an attempt to again breathe life into the Observable proposal with the hope
530 | of shipping a version of it to the Web Platform.
531 |
532 | ### Userland libraries
533 |
534 | In [prior discussion](https://github.com/whatwg/dom/issues/544#issuecomment-1433955626),
535 | [Ben Lesh](https://github.com/benlesh) has listed several custom userland implementations of
536 | observable primitives, of which RxJS is the most popular with "47,000,000+ downloads _per week_."
537 |
538 | - [RxJS](https://github.com/ReactiveX/rxjs/blob/9ddc27dd60ac23e95b2503716ae8013e64275915/src/internal/Observable.ts#L10): Started as a reference implementation of the TC39 proposal, is nearly identical to this proposal's observable.
539 | - [Relay](https://github.com/facebook/relay/blob/af8a619d7f61ea6e2e26dd4ac4ab1973d68e6ff9/packages/relay-runtime/network/RelayObservable.js): A mostly identical contract with the addition of `start` and `unsubscribe` events for observation and acquiring the `Subscription` prior to the return.
540 | - [tRPC](https://github.com/trpc/trpc/blob/21bcb5e6723023d3acb0b836b63627922407c682/packages/server/src/observable/observable.ts): A nearly identical implemention of observable to this proposal.
541 | - [XState](https://github.com/statelyai/xstate/blob/754afa022047518ef4813f7aa85398218b39f960/packages/core/src/types.ts#L1711C19-L1737): uses an observable interface in several places in their library, in particular for their `Actor` type, to allow [subscriptions to changes in state, as shown in their `useActor` hook](https://github.com/statelyai/xstate/blob/754afa022047518ef4813f7aa85398218b39f960/packages/xstate-solid/src/useActor.ts#L47-L51). Using an identical observable is also a [documented part](https://github.com/statelyai/xstate/blob/754afa022047518ef4813f7aa85398218b39f960/packages/xstate-solid/README.md?plain=1#L355-L368) of access state machine changes when using XState with SolidJS.
542 | - [SolidJS](https://github.com/solidjs/solid/blob/46e5e78710cdd9f170a7afd0ddc5311676d3532a/packages/solid/src/reactive/observable.ts#L46): An identical interface to this proposal is exposed for users to use.
543 | - [Apollo GraphQL](https://github.com/apollographql/apollo-client/blob/a1dac639839ffc5c2de332db2ee4b29bb0723815/src/utilities/observables/Observable.ts): Actually re-exporting from [zen-observable](https://github.com/zenparsing/es-observable) as [their own thing](https://github.com/apollographql/apollo-client/blob/a1dac639839ffc5c2de332db2ee4b29bb0723815/src/core/index.ts#L76), giving some freedom to reimplement on their own or pivot to something like RxJS observable at some point.
544 | - [zen-observable](https://github.com/zenparsing/zen-observable/tree/8406a7e3a3a3faa080ec228b9a743f48021fba8b): A reference implementation of the TC39 observable proposal. Nearly identical to this proposal.
545 | - [React Router](https://github.com/remix-run/react-router/tree/610ce6edf0993384300ff3172fc6db00ead50d33): Uses a `{ subscribe(callback: (value: T) => void): () => void }` pattern in their [Router](https://github.com/remix-run/react-router/blob/610ce6edf0993384300ff3172fc6db00ead50d33/packages/router/router.ts#L931) and [DeferredData](https://github.com/remix-run/react-router/blob/610ce6edf0993384300ff3172fc6db00ead50d33/packages/router/utils.ts#L1338) code. This was pointed out by maintainers as being inspired by Observable.
546 | - [Preact](https://github.com/preactjs/preact/blob/ac1f145877a74e49f4c341e6acbf888a96e60afe/src/jsx.d.ts#LL69C1-L73C3) Uses a `{ subscribe(callback: (value: T) => void): () => void }` interface for their signals.
547 | - [TanStack](https://github.com/TanStack/query/blob/878d85e44c984822e2e868af94003ec260ddf80f/packages/query-core/src/subscribable.ts): Uses a subscribable interface that matches `{ subscribe(callback: (value: T) => void): () => void }` in [several places](https://github.com/search?q=repo%3ATanStack/query%20Subscribable&type=code)
548 | - [Redux](https://github.com/reduxjs/redux/blob/c2b9785fa78ad234c4116cf189877dbab38e7bac/src/createStore.ts#LL344C12-L344C22): Implements an observable that is nearly identical to this proposal's observable as a means of subscribing to changes to a store.
549 | - [Svelte](https://github.com/sveltejs/svelte): Supports [subscribing](https://github.com/sveltejs/svelte/blob/3bc791bcba97f0810165c7a2e215563993a0989b/src/runtime/internal/utils.ts#L69) to observables that fit this exact contract, and also exports and uses a [subscribable contract for stores](https://github.com/sveltejs/svelte/blob/3bc791bcba97f0810165c7a2e215563993a0989b/src/runtime/store/index.ts) like `{ subscribe(callback: (value: T) => void): () => void }`.
550 | - [Dexie.js](https://github.com/dexie/Dexie.js): Has an [observable implementation](https://github.com/solidjs/solid/blob/46e5e78710cdd9f170a7afd0ddc5311676d3532a/packages/solid/src/reactive/observable.ts#L46) that is used for creating [live queries](https://github.com/dexie/Dexie.js/blob/bf9004b26228e43de74f7c1fa7dd60bc9d785e8d/src/live-query/live-query.ts#L36) to IndexedDB.
551 | - [MobX](https://github.com/mobxjs/mobx): Uses [similar interface](https://github.com/mobxjs/mobx/blob/7cdc7ecd6947a6da10f10d2e4a1305297b816007/packages/mobx/src/types/observableobject.ts#L583) to Observable internally for observation: `{ observe_(callback: (value: T)): () => void }`.
552 |
553 | ### UI Frameworks Supporting Observables
554 |
555 | - [Svelte](https://github.com/sveltejs/svelte): Directly supports implicit subscription and unsubscription to observables simply by binding to them in templates.
556 | - [Angular](https://github.com/angular/angular): Directly supports implicit subscription and unsubscription to observables using their `| async` "async pipe" functionality in templates.
557 | - [Vue](https://github.com/vuejs/vuejs): maintains a [dedicated library](https://github.com/vuejs/vue-rx) specifically for using Vue with RxJS observables.
558 | - [Cycle.js](https://github.com/cyclejs/cyclejs): A UI framework built entirely around observables
559 |
560 | Given the extensive prior art in this area, there exists a public
561 | "[Observable Contract](https://reactivex.io/documentation/contract.html)".
562 |
563 | Additionally many JavaScript APIs been trying to adhere to the contract defined by the [TC39 proposal from 2015](https://github.com/tc39/proposal-observable).
564 | To that end, there is a library, [symbol-observable](https://www.npmjs.com/package/symbol-observable?activeTab=dependents),
565 | that ponyfills (polyfills) `Symbol.observable` to help with interoperability between observable types that adheres to exactly
566 | the interface defined here. `symbol-observable` has 479 dependent packages on npm, and is downloaded more than 13,000,000 times
567 | per week. This means that there are a minimum of 479 packages on npm that are using the observable contract in some way.
568 |
569 | This is similar to how [Promises/A+](https://promisesaplus.com/) specification that was developed before `Promise`s were
570 | adopted into ES2015 as a first-class language primitive.
571 |
572 | ## Concerns
573 |
574 | One of the main [concerns](https://github.com/whatwg/dom/issues/544#issuecomment-351443624)
575 | expressed in the original WHATWG DOM thread has to do with Promise-ifying APIs on Observable,
576 | such as the proposed `first()`. The potential footgun here with microtask scheduling and event
577 | integration. Specifically, the following innocent-looking code would not _always_ work:
578 |
579 | ```js
580 | element
581 | .when('click')
582 | .first()
583 | .then((e) => {
584 | e.preventDefault();
585 | // Do something custom...
586 | });
587 | ```
588 |
589 | If `Observable#first()` returns a Promise that resolves when the first event is fired on an
590 | `EventTarget`, then the user-supplied Promise `.then()` handler will run:
591 |
592 | - ✅ Synchronously after event firing, for events triggered by the user
593 | - ❌ Asynchronously after event firing, for all events triggered by script (i.e., `element.click()`)
594 | - This means `e.preventDefault()` will have happened too late and effectively been ignored
595 |
596 |
597 | To understand why this is the case, you must understand how and when the microtask queue is flushed
598 | (and thus how microtasks, including Promise resolution handlers, are invoked).
599 |
600 | In WebIDL after a callback is invoked, the HTML algorithm
601 | _[clean up after running script](https://html.spec.whatwg.org/C#clean-up-after-running-script)_
602 | [is called](https://webidl.spec.whatwg.org/#ref-for-clean-up-after-running-script%E2%91%A0), and
603 | this algorithm calls _[perform a microtask checkpoint](https://html.spec.whatwg.org/C#perform-a-microtask-checkpoint)_
604 | if and only if the JavaScript stack is empty.
605 |
606 | Concretely, that means for `element.click()` in the above example, the following steps occur:
607 |
608 | 1. To run `element.click()`, a JavaScript execution context is first pushed onto the stack
609 | 1. To run the internal `click` event listener callback (the one created natively by the
610 | `Observable#from()` implementation), _another_ JavaScript execution context is pushed onto
611 | the stack, as WebIDL prepares to run the internal callback
612 | 1. The internal callback runs, which immediately resolves the Promise returned by `Observable#first()`;
613 | now the microtask queue contains the Promise's user-supplied `then()` handler which will cancel
614 | the event once it runs
615 | 1. The top-most execution context is removed from the stack, and the microtask queue **cannot be
616 | flushed**, because there is still JavaScript on the stack.
617 | 1. After the internal `click` event callback is executed, the rest of the event path continues since
618 | event was not canceled during or immediately after the callback. The event does whatever it would
619 | normally do (submit the form, `alert()` the user, etc.)
620 | 1. Finally, the JavaScript containing `element.click()` is finished, and the final execution context
621 | is popped from the stack and the microtask queue is flushed. The user-supplied `.then()` handler
622 | is run, which attempts to cancel the event too late
623 |
624 |
625 | Two things mitigate this concern. First, there is a very simple workaround to _always_ avoid the
626 | case where your `e.preventDefault()` might run too late:
627 |
628 | ```js
629 | element
630 | .when('click')
631 | .map((e) => (e.preventDefault(), e))
632 | .first();
633 | ```
634 |
635 | ...or if Observable had a `.do()` method (see https://github.com/whatwg/dom/issues/544#issuecomment-351457179):
636 |
637 | ```js
638 | element
639 | .when('click')
640 | .do((e) => e.preventDefault())
641 | .first();
642 | ```
643 |
644 | ...or by [modifying](https://github.com/whatwg/dom/issues/544#issuecomment-351779661) the semantics of
645 | `first()` to take a callback that produces a value that the returned Promise resolves to:
646 |
647 | ```js
648 | el.when('submit')
649 | .first((e) => e.preventDefault())
650 | .then(doMoreStuff);
651 | ```
652 |
653 | Second, this "quirk" already exists in today's thriving Observable ecosystem, and there are no serious
654 | concerns or reports from that community that developers are consistently running into this. This gives
655 | some confidence that baking this behavior into the web platform will not be dangerous.
656 |
657 | ## Standards venue
658 |
659 | There's been much discussion about which standards venue should ultimately host an Observables
660 | proposal. The venue is not inconsequential, as it effectively decides whether Observables becomes a
661 | language-level primitive like `Promise`s, that ship in all JavaScript browser engines, or a web platform
662 | primitive with likely (but technically _optional_) consideration in other environments like Node.js
663 | (see [`AbortController`](https://nodejs.org/api/globals.html#class-abortcontroller) for example).
664 |
665 | Observables purposefully integrate frictionlessly with the main event-emitting interface
666 | (`EventTarget`) and cancellation primitive (`AbortController`) that live in the Web platform. As
667 | proposed here, observables join this existing strongly-connected component from the [DOM
668 | Standard](https://github.com/whatwg/dom): Observables depend on AbortController/AbortSignal, which
669 | depend on EventTarget, and EventTarget depends on both Observables and AbortController/AbortSignal.
670 | Because we feel that Observables fits in best where its supporting primitives live, the WHATWG
671 | standards venue is probably the best place to advance this proposal. Additionally, non-Web
672 | ECMAScript embedders like Node.js and Deno would still be able to adopt Observables, and are even
673 | likely to, given their commitment to Web platform [aborting and
674 | events](https://github.com/whatwg/dom/blob/bf5f6c2a8f2d770da884cb52f5625c59b5a880e7/PULL_REQUEST_TEMPLATE.md).
675 |
676 | This does not preclude future standardization of event-emitting and cancellation primitives in TC39
677 | in the future, something Observables could theoretically be layered on top of later. But for now, we
678 | are motivated to make progress in WHATWG.
679 |
680 | In attempt to avoid relitigating this discussion, we'd urge the reader to see the following
681 | discussion comments:
682 |
683 | - https://github.com/whatwg/dom/issues/544#issuecomment-351520728
684 | - https://github.com/whatwg/dom/issues/544#issuecomment-351561091
685 | - https://github.com/whatwg/dom/issues/544#issuecomment-351582862
686 | - https://github.com/whatwg/dom/issues/544#issuecomment-351607779
687 | - https://github.com/whatwg/dom/issues/544#issuecomment-351718686
688 |
689 | ## Standards issues
690 |
691 | This section bares a collection of web standards and standards positions issues
692 | used to track the Observable proposal's life outside of this repository.
693 |
694 | - [Mozilla standards
695 | position](https://github.com/mozilla/standards-positions/issues/945)
696 | - [WebKit standards
697 | position](https://github.com/WebKit/standards-positions/issues/292)
698 | - [Chrome Status](https://chromestatus.com/feature/5154593776599040)
699 | - [WinterCG](https://github.com/wintercg/proposal-common-minimum-api/issues/72)
700 | - [Node.js](https://github.com/nodejs/standards-positions/issues/1)
701 | - [W3C TAG review](https://github.com/w3ctag/design-reviews/issues/902)
702 |
703 | ## User needs
704 |
705 | Observables are designed to make event handling more ergonomic and composable.
706 | As such, their impact on end users is indirect, largely coming in the form of
707 | users having to download less JavaScript to implement patterns that developers
708 | currently use third-party libraries for. As stated [above in the
709 | explainer](https://github.com/domfarolino/observable#userland-libraries), there
710 | is a thriving userland Observables ecosystem which results in loads of excessive
711 | bytes being downloaded every day.
712 |
713 | In an attempt to codify the strong userland precedent of the Observable API,
714 | this proposal would save dozens of custom implementations from being downloaded
715 | every day.
716 |
717 | Additionally, as an API like `EventTarget`, `AbortController`, and one related
718 | to `Promise`s, it enables developers to build less-complicated event handling
719 | flows by constructing them declaratively, which may enable them to build more
720 | sound user experiences on the Web.
721 |
722 | ## Authors:
723 |
724 | - [Dominic Farolino](https://github.com/domfarolino)
725 | - [Ben Lesh](https://github.com/benlesh)
726 |
--------------------------------------------------------------------------------
/spec.bs:
--------------------------------------------------------------------------------
1 |
2 | Title: Observable
3 | Shortname: observable
4 | Repository: WICG/observable
5 | Inline Github Issues: true
6 | Group: WICG
7 | Status: CG-DRAFT
8 | Level: 1
9 | URL: https://wicg.github.io/observable/
10 | Boilerplate: omit conformance, omit feedback-header
11 | Editor: Dominic Farolino, Google https://www.google.com/, domfarolino@gmail.com, https://domfarolino.com
12 | Abstract: The Observable API provides a composable, ergonomic way of handling an asynchronous stream of events
13 | !Participate: GitHub WICG/observable (new issue, open issues)
14 | !Commits: GitHub spec.bs commits
15 | Complain About: accidental-2119 yes, missing-example-ids yes
16 | Indent: 2
17 | Default Biblio Status: current
18 | Markup Shorthands: markdown yes
19 | Assume Explicit For: yes
20 | WPT Display: open
21 |
22 |
23 |
24 |
25 |
26 | urlPrefix: https://tc39.es/ecma262/#; spec: ECMASCRIPT
27 | type: dfn
28 | text: current realm
29 | text: Object; url: sec-object-type
30 | text: normal completion; url: sec-completion-record-specification-type
31 | text: NormalCompletion; url: sec-normalcompletion
32 | text: throw completion; url: sec-completion-record-specification-type
33 | text: Iterator Record; url: sec-iterator-records
34 | url: sec-returnifabrupt-shorthands
35 | text: ?
36 | text: !
37 | type: abstract-op
38 | text: Type; url: sec-ecmascript-data-types-and-values
39 | urlPrefix: https://dom.spec.whatwg.org; spec: DOM
40 | type: dfn
41 | for: event listener
42 | text: type; url: event-listener-type
43 | text: capture; url: event-listener-capture
44 | text: passive; url: event-listener-passive
45 | text: once; url: event-listener-once
46 | text: signal; url: event-listener-signal
47 | for: AbortSignal
48 | text: dependent signals; url: abortsignal-dependent-signals
49 | text: signal abort; url:abortsignal-signal-abort
50 | text: abort reason; url:abortsignal-abort-reason
51 | urlPrefix: https://webidl.spec.whatwg.org; spec: WEBIDL
52 | type: dfn
53 | text: a promise rejected with
54 |
55 |
56 |
152 |
153 |
154 |
155 | Introduction
156 |
157 | *This section is non-normative.*
158 |
159 | Core infrastructure
160 |
161 | The {{Subscriber}} interface
162 |
163 |
164 | [Exposed=*]
165 | interface Subscriber {
166 | undefined next(any value);
167 | undefined error(any error);
168 | undefined complete();
169 | undefined addTeardown(VoidFunction teardown);
170 |
171 | // True after the Subscriber is created, up until either
172 | // complete()/error() are invoked, or the subscriber unsubscribes. Inside
173 | // complete()/error(), this attribute is true.
174 | readonly attribute boolean active;
175 |
176 | readonly attribute AbortSignal signal;
177 | };
178 |
179 |
180 | Each {{Subscriber}} has an [=ordered set=] of internal observers,
181 | initially empty.
182 |
183 | Each {{Subscriber}} has a teardown callbacks, which is a [=list=] of
184 | {{VoidFunction}}s, initially empty.
185 |
186 | Each {{Subscriber}} has a subscription controller, which is an
187 | {{AbortController}}.
188 |
189 | Each {{Subscriber}} has a active boolean, initially true.
190 |
191 | Note: This is a bookkeeping variable to ensure that a {{Subscriber}} never calls any of the
192 | callbacks it owns after it has been [=close a subscription|closed=].
193 |
194 | The active getter steps are to return [=this=]'s
195 | [=Subscriber/active=] boolean.
196 |
197 | The signal getter steps are to return [=this=]'s
198 | [=Subscriber/subscription controller=]'s [=AbortController/signal=].
199 |
200 |
201 | The
next(|value|) method steps are:
202 |
203 | 1. If [=this=]'s [=Subscriber/active=] is false, then return.
204 |
205 | 1. If [=this=]'s [=relevant global object=] is a {{Window}} object, and its [=associated
206 | Document=] is not [=Document/fully active=], then return.
207 |
208 | 1. Let |internal observers copy| be a copy of [=this=]'s [=Subscriber/internal observers=].
209 |
210 | Note: We make a copy of the [=Subscriber/internal observers=] list and iterate over it so
211 | that if one of the [=internal observer=]'s [=internal observer/next steps=] below causes
212 | another subscription, the observer list does not mutate during iteration.
213 |
214 | 1. [=set/For each=] |observer| of |internal observers copy|:
215 |
216 | 1. Run |observer|'s [=internal observer/next steps=] given |value|.
217 |
218 | [=Assert=]: No
exception was thrown.
219 |
220 |
221 |
Note: No exception can be thrown here because in the case where the
222 | [=Subscriber/internal observer=]'s [=internal observer/next steps=] is just a wrapper
223 | around a script-provided callback, the process observer
224 | steps take care to wrap these callbacks in logic that, when invoking them, catches any
225 | exceptions, and reports them to the global.
226 |
227 |
When the [=internal observer/next steps=] is a spec algorithm, those steps take care
228 | to not throw any exceptions outside of itself, to appease this assert.
229 |
230 |
231 |
232 |
233 | The
error(|error|) method steps are:
234 |
235 | 1. If [=this=]'s [=Subscriber/active=] is false, [=report an exception=] with |error| and
236 | [=this=]'s [=relevant global object=], then return.
237 |
238 | 1. If [=this=]'s [=relevant global object=] is a {{Window}} object, and its [=associated
239 | Document=] is not [=Document/fully active=], then return.
240 |
241 | 1. [=close a subscription|Close=] [=this=].
242 |
243 | 1. Let |internal observers copy| be a copy of [=this=]'s [=Subscriber/internal observers=].
244 |
245 | 1. [=set/For each=] |observer| of |internal observers copy|:
246 |
247 | 1. Run |observer|'s [=internal observer/error steps=] given |error|.
248 |
249 | [=Assert=]: No
exception was thrown.
250 |
251 | Note: See the documentation in {{Subscriber/next()}} for details on why this is true.
252 |
253 |
254 |
255 | The
complete() method steps are:
256 |
257 | 1. If [=this=]'s [=Subscriber/active=] is false, then return.
258 |
259 | 1. If [=this=]'s [=relevant global object=] is a {{Window}} object, and its [=associated
260 | Document=] is not [=Document/fully active=], then return.
261 |
262 | 1. [=close a subscription|Close=] [=this=].
263 |
264 | 1. Let |internal observers copy| be a copy of [=this=]'s [=Subscriber/internal observers=].
265 |
266 | 1. [=set/For each=] |observer| of |internal observers copy|:
267 |
268 | 1. Run |observer|'s [=internal observer/complete steps=].
269 |
270 | [=Assert=]: No
exception was thrown.
271 |
272 | Note: See the documentation in {{Subscriber/next()}} for details on why this is true.
273 |
274 |
275 |
276 | The addTeardown(|teardown|) method steps are:
277 |
278 | 1. If [=this=]'s [=relevant global object=] is a {{Window}} object, and its [=associated
279 | Document=] is not [=Document/fully active=], then return.
280 |
281 | 1. If [=this=]'s [=Subscriber/active=] is true, then [=list/append=] |teardown| to [=this=]'s
282 | [=Subscriber/teardown callbacks=] list.
283 |
284 | 1. Otherwise, [=invoke=] |teardown| with «» and "`report`".
285 |
286 |
287 |
288 | To
close a subscription given a {{Subscriber}} |subscriber|, and an optional {{any}}
289 | |reason|, run these steps:
290 |
291 | 1. If |subscriber|'s [=Subscriber/active=] is false, then return.
292 |
293 |
294 |
This guards against re-entrant invocation, which can happen in the "producer-initiated"
295 | unsubscription case. Consider the following example:
296 |
297 |
298 | const outerController = new AbortController();
299 | const observable = new Observable(subscriber => {
300 | subscriber.addTeardown(() => {
301 | // 2.) This teardown executes inside the "Close" algorithm, while it's
302 | // running. Aborting the downstream signal run its abort algorithms,
303 | // one of which is the currently-running "Close" algorithm.
304 | outerController.abort();
305 | });
306 |
307 | // 1.) This immediately invokes the "Close" algorithm, which
308 | // sets subscriber.active to false.
309 | subscriber.complete();
310 | });
311 |
312 | observable.subscribe({}, {signal: outerController.signal});
313 |
314 |
315 |
316 |
317 | 1. Set |subscriber|'s [=Subscriber/active=] boolean to false.
318 |
319 | 1. [=AbortSignal/Signal abort=] |subscriber|'s [=Subscriber/subscription controller=] with
320 | |reason|, if it is given.
321 |
322 | 1. [=list/For each=] |teardown| of |subscriber|'s [=Subscriber/teardown callbacks=] sorted in
323 | reverse insertion order:
324 |
325 | 1. If |subscriber|'s [=relevant global object=] is a {{Window}} object, and its [=associated
326 | Document=] is not [=Document/fully active=], then abort these steps.
327 |
328 | Note: This step runs repeatedly because each |teardown| could result in the above
329 | {{Document}} becoming inactive.
330 |
331 | 1. [=Invoke=] |teardown| with «» and "`report`".
332 |
333 |
334 | The {{Observable}} interface
335 |
336 |
337 | // SubscribeCallback is where the Observable "creator's" code lives. It's
338 | // called when subscribe() is called, to set up a new subscription.
339 | callback SubscribeCallback = undefined (Subscriber subscriber);
340 | callback ObservableSubscriptionCallback = undefined (any value);
341 |
342 | dictionary SubscriptionObserver {
343 | ObservableSubscriptionCallback next;
344 | ObservableSubscriptionCallback error;
345 | VoidFunction complete;
346 | };
347 |
348 | callback ObservableInspectorAbortHandler = undefined (any value);
349 |
350 | dictionary ObservableInspector {
351 | ObservableSubscriptionCallback next;
352 | ObservableSubscriptionCallback error;
353 | VoidFunction complete;
354 |
355 | VoidFunction subscribe;
356 | ObservableInspectorAbortHandler abort;
357 | };
358 |
359 | typedef (ObservableSubscriptionCallback or SubscriptionObserver) ObserverUnion;
360 | typedef (ObservableSubscriptionCallback or ObservableInspector) ObservableInspectorUnion;
361 |
362 | dictionary SubscribeOptions {
363 | AbortSignal signal;
364 | };
365 |
366 | callback Predicate = boolean (any value, unsigned long long index);
367 | callback Reducer = any (any accumulator, any currentValue, unsigned long long index);
368 | callback Mapper = any (any value, unsigned long long index);
369 | // Differs from Mapper only in return type, since this callback is exclusively
370 | // used to visit each element in a sequence, not transform it.
371 | callback Visitor = undefined (any value, unsigned long long index);
372 |
373 | // This callback returns an `any` that must convert into an `Observable`, via
374 | // the `Observable` conversion semantics.
375 | callback CatchCallback = any (any value);
376 |
377 | [Exposed=*]
378 | interface Observable {
379 | constructor(SubscribeCallback callback);
380 | undefined subscribe(optional ObserverUnion observer = {}, optional SubscribeOptions options = {});
381 |
382 | // Constructs a native Observable from value if it's any of the following:
383 | // - Observable
384 | // - AsyncIterable
385 | // - Iterable
386 | // - Promise
387 | static Observable from(any value);
388 |
389 | // Observable-returning operators. See "Operators" section in the spec.
390 | //
391 | // takeUntil() can consume promises, iterables, async iterables, and other
392 | // observables.
393 | Observable takeUntil(any value);
394 | Observable map(Mapper mapper);
395 | Observable filter(Predicate predicate);
396 | Observable take(unsigned long long amount);
397 | Observable drop(unsigned long long amount);
398 | Observable flatMap(Mapper mapper);
399 | Observable switchMap(Mapper mapper);
400 | Observable inspect(optional ObservableInspectorUnion inspectorUnion = {});
401 | Observable catch(CatchCallback callback);
402 | Observable finally(VoidFunction callback);
403 |
404 | // Promise-returning operators.
405 | Promise> toArray(optional SubscribeOptions options = {});
406 | Promise forEach(Visitor callback, optional SubscribeOptions options = {});
407 | Promise every(Predicate predicate, optional SubscribeOptions options = {});
408 | Promise first(optional SubscribeOptions options = {});
409 | Promise last(optional SubscribeOptions options = {});
410 | Promise find(Predicate predicate, optional SubscribeOptions options = {});
411 | Promise some(Predicate predicate, optional SubscribeOptions options = {});
412 | Promise reduce(Reducer reducer, optional any initialValue, optional SubscribeOptions options = {});
413 | };
414 |
415 |
416 | Each {{Observable}} has a subscribe callback, which is a
417 | {{SubscribeCallback}} or a set of steps that take in a {{Subscriber}}.
418 |
419 | Each {{Observable}} has a weak subscriber, which is a weak reference to a
420 | {{Subscriber}}-or-null, initially null.
421 |
422 | Note: The "union" of these types is to support both {{Observable}}s created by JavaScript (that are
423 | always constructed with a {{SubscribeCallback}}), and natively-constructed {{Observable}} objects
424 | (whose [=Observable/subscribe callback=] could be an arbitrary set of native steps, not a JavaScript
425 | callback). The return value of {{EventTarget/when()}} is an example of the latter.
426 |
427 |
428 | The new
429 | Observable(|callback|) constructor steps are:
430 |
431 | 1. Set [=this=]'s [=Observable/subscribe callback=] to |callback|.
432 |
433 | Note: This callback will get invoked later when {{Observable/subscribe()}} is called.
434 |
435 |
436 |
437 | The
subscribe(|observer|, |options|) method steps
438 | are:
439 |
440 | 1.
Subscribe to [=this=] given |observer|
441 | and |options|.
442 |
443 |
444 | Supporting concepts
445 |
446 |
447 | The
default error algorithm is an algorithm that takes an {{any}} |error|, and runs
448 | these steps:
449 |
450 | 1. [=Report an exception=] with |error| and the [=current realm=]'s [=realm/global object=].
451 |
452 | Note: We pull this default out separately so that every place in this specification that natively
453 |
subscribes to an {{Observable}} (i.e.,
454 | subscribes from spec prose, not going through the {{Observable/subscribe()}} method) doesn't have
455 | to redundantly define these steps.
456 |
457 |
458 | An internal observer is a [=struct=] with the following [=struct/items=]:
459 |
460 |
461 | : next steps
462 | :: An algorithm that takes a single parameter of type {{any}}. Initially, these steps do nothing.
463 |
464 | : error steps
465 | :: An algorithm that takes a single parameter of type {{any}}. Initially, the [=default error
466 | algorithm=].
467 |
468 | : complete steps
469 | :: An algorithm with no parameters. Initially, these steps do nothing.
470 |
471 |
472 |
473 |
The [=internal observer=] [=struct=] is used to mirror the {{SubscriptionObserver/next}},
474 | {{SubscriptionObserver/error}}, and {{SubscriptionObserver/complete}} [=callback functions=]. For
475 | any {{Observable}} that is subscribed by JavaScript via the {{Observable/subscribe()}} method,
476 | these algorithm "steps" will just be a wrapper around [=invoking=] the corresponding
477 | {{SubscriptionObserver/next}}, {{SubscriptionObserver/error}}, and
478 | {{SubscriptionObserver/complete}} [=callback functions=] provided by script.
479 |
480 |
But when internal spec prose (not user script) subscribes to an {{Observable}}, these "steps" are arbitrary spec algorithms that
482 | are not provided via an {{ObserverUnion}} packed with Web IDL [=callback functions=]. See the
483 | [[#promise-returning-operators]] that make use of this, for example.
484 |
485 |
486 |
487 | To
convert to an Observable an {{any}} |value|, run these steps:
488 |
489 | Note: We split this algorithm out from the Web IDL {{Observable/from()}} method, so that spec
490 | prose can
convert values to without going
491 | through the Web IDL bindings.
492 |
493 | 1. If [$Type$](|value|) is not [=Object=], [=exception/throw=] a {{TypeError}}.
494 |
495 | Note: This prevents primitive types from being coerced into iterables (e.g., String). See
496 | discussion in
WICG/observable#125.
497 |
498 | 1.
From Observable: If |value|'s [=specific type=]
499 | is an {{Observable}}, then return |value|.
500 |
501 | 1.
From async iterable: Let
502 | |asyncIteratorMethod| be [=?=] [$GetMethod$](|value|, {{%Symbol.asyncIterator%}}).
503 |
504 | Note: We use [$GetMethod$] instead of [$GetIterator$] because we're only probing for async
505 | iterator protocol support, and we don't want to throw if it's not implemented.
506 | [$GetIterator$] throws errors in BOTH of the following cases: (a) no iterator protocol is
507 | implemented, (b) an iterator protocol is implemented, but isn't callable or its getter
508 | throws. [$GetMethod$] lets us ONLY throw in the latter case.
509 |
510 | 1. If |asyncIteratorMethod|'s is undefined or null, then jump to the step labeled
From iterable.
512 |
513 | 1. Let |nextAlgorithm| be the following steps, given a {{Subscriber}} |subscriber| and an
514 | [=Iterator Record=] |iteratorRecord|:
515 |
516 | 1. If |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=] is
517 | [=AbortSignal/aborted=], then return.
518 |
519 | 1. Let |nextPromise| be a {{Promise}}-or-undefined, initially undefined.
520 |
521 | 1. Let |nextCompletion| be [$IteratorNext$](|iteratorRecord|).
522 |
523 | Note: We use [$IteratorNext$] here instead of [$IteratorStepValue$], because
524 | [$IteratorStepValue$] expects the iterator's `next()` method to return an object that
525 | can immediately be inspected for a value, whereas in the async iterator case, `next()`
526 | is expected to return a Promise/thenable (which we wrap in a Promise and react to to get
527 | that value).
528 |
529 | 1. If |nextCompletion| is a [=throw completion=], then:
530 |
531 | 1. [=Assert=]: |iteratorRecord|'s \[[Done]] is true.
532 |
533 | 1. Set |nextPromise| to [=a promise rejected with=] |nextRecord|'s \[[Value]].
534 |
535 | 1. Otherwise, if |nextRecord| is [=normal completion=], then set |nextPromise| to [=a
536 | promise resolved with=] |nextRecord|'s \[[Value]].
537 |
538 | Note: This is done in case |nextRecord|'s \[[Value]] is not *itself* already a
539 | {{Promise}}.
540 |
541 | 1. [=promise/React=] to |nextPromise|:
542 |
543 | * If |nextPromise| was fulfilled with value |iteratorResult|, then:
544 |
545 | 1. If [$Type$](|iteratorResult|) is not Object, then run |subscriber|'s
546 | {{Subscriber/error()}} method with a {{TypeError}} and abort these steps.
547 |
548 | 1. Let |done| be [$IteratorComplete$](|iteratorResult|).
549 |
550 | 1. If |done| is a [=throw completion=], then run |subscriber|'s
551 | {{Subscriber/error()}} method with |done|'s \[[Value]] and abort these steps.
552 |
553 | 1. If |done|'s \[[Value]] is true, then run |subscriber|'s {{Subscriber/complete()}}
554 | and abort these steps.
555 |
556 | 1. Let |value| be [$IteratorValue$](|iteratorResult|).
557 |
558 | 1. If |value| is a [=throw completion=], then run |subscriber|'s
559 | {{Subscriber/error()}} method with |value|'s \[[Value]] and abort these steps.
560 |
561 | 1. Run |subscriber|'s {{Subscriber/next()}} given |value|'s \[[Value]].
562 |
563 | 1. Run |nextAlgorithm| given |subscriber| and |iteratorRecord|.
564 |
565 | * If |nextPromise| was rejected with reason |r|, then run |subscriber|'s
566 | {{Subscriber/error()}} method given |r|.
567 |
568 | 1. Return a [=new=] {{Observable}} whose [=Observable/subscribe callback=] is an algorithm that
569 | takes a {{Subscriber}} |subscriber| and does the following:
570 |
571 | 1. If |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=] is
572 | [=AbortSignal/aborted=], then return.
573 |
574 | 1. Let |iteratorRecordCompletion| be [$GetIterator$](|value|, async).
575 |
576 | Note: This both re-invokes any {{%Symbol.asyncIterator%}} method getters on |value|—note
577 | that whether this is desirable is an extreme corner case, but it matches test
578 | expectations; see
issue#127 for
579 | discussion—and invokes the protocol itself to obtain an [=Iterator Record=].
580 |
581 | 1. If |iteratorRecordCompletion| is a [=throw completion=], then run |subscriber|'s
582 | {{Subscriber/error()}} method with |iteratorRecordCompletion|'s \[[Value]] and abort these
583 | steps.
584 |
585 | Note: This means we invoke the {{Subscriber/error()}} method synchronously with respect to
586 | subscription, which is the only time this can happen for async iterables that are
587 | converted to {{Observable}}s. In all other cases, errors are propagated to the observer
588 | asynchronously, with microtask timing, by virtue of being wrapped in a rejected
589 | {{Promise}} that |nextAlgorithm| [=promise/reacts=] to. This synchronous-error-propagation
590 | behavior is consistent with language constructs, i.e., **for-await of** loops that invoke
591 | {{%Symbol.asyncIterator%}} and synchronously re-throw exceptions to catch blocks outside
592 | the loop, before any [$Await|Awaiting$] takes place.
593 |
594 | 1. Let |iteratorRecord| be [=!=] |iteratorRecordCompletion|.
595 |
596 | 1. [=Assert=]: |iteratorRecord| is an [=Iterator Record=].
597 |
598 | 1. If |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=] is
599 | [=AbortSignal/aborted=], then return.
600 |
601 | 1. [=AbortSignal/add|Add the following abort algorithm=] to |subscriber|'s
602 | [=Subscriber/subscription controller=]'s [=AbortController/signal=]:
603 |
604 | 1. Run [$AsyncIteratorClose$](|iteratorRecord|, [=NormalCompletion=](|subscriber|'s
605 | [=Subscriber/subscription controller=]'s [=AbortSignal/abort reason=])).
606 |
607 | 1. Run |nextAlgorithm| given |subscriber| and |iteratorRecord|.
608 |
609 | 1.
From iterable: Let |iteratorMethod| be [=?=]
610 | [$GetMethod$](|value|, {{%Symbol.iterator%}}).
611 |
612 | 1. If |iteratorMethod| is undefined, then jump to the step labeled
From Promise.
614 |
615 | Otherwise, return a [=new=] {{Observable}} whose [=Observable/subscribe callback=] is an
616 | algorithm that takes a {{Subscriber}} |subscriber| and does the following:
617 |
618 | 1. If |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=] is
619 | [=AbortSignal/aborted=], then return.
620 |
621 | 1. Let |iteratorRecordCompletion| be [$GetIterator$](|value|, sync).
622 |
623 | 1. If |iteratorRecordCompletion| is a [=throw completion=], then run |subscriber|'s
624 | {{Subscriber/error()}} method, given |iteratorRecordCompletion|'s \[[Value]], and abort
625 | these steps.
626 |
627 | 1. Let |iteratorRecord| be [=!=] |iteratorRecordCompletion|.
628 |
629 | 1. If |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=] is
630 | [=AbortSignal/aborted=], then return.
631 |
632 | 1. [=AbortSignal/add|Add the following abort algorithm=] to |subscriber|'s
633 | [=Subscriber/subscription controller=]'s [=AbortController/signal=]:
634 |
635 | 1. Run [$IteratorClose$](|iteratorRecord|, [=NormalCompletion=](UNUSED)).
636 |
637 | 1. [=iteration/While=] true:
638 |
639 | 1. Let |next| be [$IteratorStepValue$](|iteratorRecord|).
640 |
641 | 1. If |next| is a [=throw completion=], then run |subscriber|'s {{Subscriber/error()}}
642 | method, given |next|'s \[[Value]], and [=iteration/break=].
643 |
644 | 1. Set |next| to [=!=] to |next|.
645 |
646 | 1. If |next| is done, then:
647 |
648 | 1. [=Assert=]: |iteratorRecord|'s \[[Done]] is true.
649 |
650 | 2. Run |subscriber|'s {{Subscriber/complete()}}.
651 |
652 | 3. Return.
653 |
654 | 1. Run |subscriber|'s {{Subscriber/next()}} given |next|.
655 |
656 | 1. If |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=]
657 | is [=AbortSignal/aborted=], then [=iteration/break=].
658 |
659 | 1.
From Promise: If [$IsPromise$](|value|) is true,
660 | then:
661 |
662 | 1. Return a [=new=] {{Observable}} whose [=Observable/subscribe callback=] is an algorithm
663 | that takes a {{Subscriber}} |subscriber| and does the following:
664 |
665 | 1. [=promise/React=] to |value|:
666 |
667 | 1. If |value| was fulfilled with value |v|, then:
668 |
669 | 1. Run |subscriber|'s {{Subscriber/next()}} method, given |v|.
670 |
671 | 1. Run |subscriber|'s {{Subscriber/complete()}} method.
672 |
673 | 1. If |value| was rejected with reason |r|, then run |subscriber|'s
674 | {{Subscriber/error()}} method, given |r|.
675 |
676 | 1. [=exception/Throw=] a {{TypeError}}.
677 |
678 |
679 | /dom/observable/tentative/observable-from.any.js
680 |
681 |
682 |
683 |
684 | To
subscribe to an {{Observable}} given an
685 | {{ObserverUnion}}-or-[=internal observer=] |observer|, and a {{SubscribeOptions}} |options|, run
686 | these steps:
687 |
688 | Note: We split this algorithm out from the Web IDL {{Observable/subscribe()}} method, so that spec
689 | prose can
subscribe to an {{Observable}}
690 | without going through the Web IDL bindings. See
w3c/IntersectionObserver#464 for
692 | similar context, where "internal" prose
must not go through Web IDL
693 | bindings on objects whose properties could be mutated by JavaScript. See
694 | [[#promise-returning-operators]] for usage of this.
695 |
696 | 1. If [=this=]'s [=relevant global object=] is a {{Window}} object, and its [=associated
697 | Document=] is not [=Document/fully active=], then return.
698 |
699 | 1. Let |internal observer| be a new [=internal observer=].
700 |
701 | 1. Process |observer| as follows:
702 |
703 | -
704 |
705 | - If |observer| is an {{ObservableSubscriptionCallback}}
706 | - Set |internal observer|'s [=internal observer/next steps=] to these steps that take
707 | an {{any}} |value|:
708 |
709 | 1. [=Invoke=] |observer| with «|value|» and "`report`".
710 |
711 |
712 | - If |observer| is a {{SubscriptionObserver}}
713 | -
714 | 1. If |observer|'s {{SubscriptionObserver/next}} [=map/exists=], then set |internal
715 | observer|'s [=internal observer/next steps=] to these steps that take an {{any}}
716 | |value|:
717 |
718 | 1. [=Invoke=] |observer|'s {{SubscriptionObserver/next}} with «|value|» and
719 | "`report`".
720 |
721 | 1. If |observer|'s {{SubscriptionObserver/error}} [=map/exists=], then set |internal
722 | observer|'s [=internal observer/error steps=] to these steps that take an {{any}}
723 | |error|:
724 |
725 | 1. [=Invoke=] |observer|'s {{SubscriptionObserver/error}} with «|error|» and
726 | "`report`".
727 |
728 | 1. If |observer|'s {{SubscriptionObserver/complete}} [=map/exists=], then set
729 | |internal observer|'s [=internal observer/complete steps=] to these steps:
730 |
731 | 1. [=Invoke=] |observer|'s {{SubscriptionObserver/complete}} with «» and
732 | "`report`".
733 |
734 |
735 | - If |observer| is an [=internal observer=]
736 | - Set |internal observer| to |observer|.
737 |
738 |
739 |
740 |
741 | 1. [=Assert=]: |internal observer|'s [=internal observer/error steps=] is either the [=default
742 | error algorithm=], or an algorithm that [=invokes=] the provided
743 | {{SubscriptionObserver/error}} [=callback function=].
744 |
745 | 1. If [=this=]'s [=Observable/weak subscriber=] is not null and [=this=]'s [=Observable/weak
746 | subscriber=]'s [=Subscriber/active=] is true:
747 |
748 | 1. Let |subscriber| be [=this=]'s [=Observable/weak subscriber=].
749 |
750 | 1. [=set/Append=] |internal observer| to |subscriber|'s [=Subscriber/internal observers=].
751 |
752 | 1. If |options|'s {{SubscribeOptions/signal}} [=map/exists=], then:
753 |
754 | 1. If |options|'s {{SubscribeOptions/signal}} is [=AbortSignal/aborted=], then
755 | [=set/remove=] |internal observer| from |subscriber|'s [=Subscriber/internal
756 | observers=].
757 |
758 | 1. Otherwise, [=AbortSignal/add|add the following abort algorithm=] to |options|'s
759 | {{SubscribeOptions/signal}}:
760 |
761 | 1. If |subscriber|'s [=Subscriber/active=] is false, then abort these steps.
762 |
763 | 1. [=set/Remove=] |internal observer| from |subscriber|'s [=Subscriber/internal
764 | observers=].
765 |
766 | 1. If |subscriber|'s [=Subscriber/internal observers=] is [=set/empty=], then [=close a
767 | subscription|close=] |subscriber| with |options|'s {{SubscribeOptions/signal}}'s
768 | [=AbortSignal/abort reason=].
769 |
770 | 1. Return.
771 |
772 | 1. Let |subscriber| be a [=new=] {{Subscriber}}.
773 |
774 | 1. [=set/Append=] |internal observer| to |subscriber|'s [=Subscriber/internal observers=].
775 |
776 | 1. Set [=this=]'s [=Observable/weak subscriber=] to |subscriber|.
777 |
778 | 1. If |options|'s {{SubscribeOptions/signal}} [=map/exists=], then:
779 |
780 | 1. If |options|'s {{SubscribeOptions/signal}} is [=AbortSignal/aborted=], then [=close a
781 | subscription|close=] |subscriber| given |options|'s {{SubscribeOptions/signal}}
782 | [=AbortSignal/abort reason=].
783 |
784 | 1. Otherwise, [=AbortSignal/add|add the following abort algorithm=] to |options|'s
785 | {{SubscribeOptions/signal}}:
786 |
787 | 1. If |subscriber|'s [=Subscriber/active=] is false, then abort these steps.
788 |
789 | 1. [=set/Remove=] |internal observer| from |subscriber|'s [=Subscriber/internal
790 | observers=].
791 |
792 | 1. If |subscriber|'s [=Subscriber/internal observers=] is [=set/empty=], then [=close a
793 | subscription|close=] |subscriber| with |options|'s {{SubscribeOptions/signal}}'s
794 | [=AbortSignal/abort reason=].
795 |
796 | 1. If [=this=]'s [=Observable/subscribe callback=] is a {{SubscribeCallback}}, then [=invoke=]
797 | it with «subscriber» with "`rethrow`".
798 |
799 | If
an exception |E| was thrown, call
800 | |subscriber|'s {{Subscriber/error()}} method with |E|.
801 |
802 | 1. Otherwise, run the steps given by [=this=]'s [=Observable/subscribe callback=], given
803 | |subscriber|.
804 |
805 |
806 |
807 | /dom/observable/tentative/observable-constructor.any.js
808 | /dom/observable/tentative/observable-constructor.window.js
809 |
810 |
811 |
812 | Operators
813 |
814 | For now, see
815 | [https://github.com/wicg/observable#operators](https://github.com/wicg/observable#operators).
816 |
817 | {{Observable/from()}}
818 |
819 |
820 | The
from(|value|) method steps are:
821 |
822 | 1. Return the result of
converting |value|
823 | to an {{Observable}}. Rethrow any exceptions.
824 |
825 |
826 | {{Observable}}-returning operators
827 |
828 |
829 | The
takeUntil(|value|) method steps are:
830 |
831 | 1. Let |sourceObservable| be [=this=].
832 |
833 | 1. Let |notifier| be the result of
834 | converting |value| to an {{Observable}}.
835 |
836 | 1. Let |observable| be a [=new=] {{Observable}} whose [=Observable/subscribe callback=] is an
837 | algorithm that takes a {{Subscriber}} |subscriber| and does the following:
838 |
839 |
840 | Note that this method involves
Subscribing to two {{Observable}}s: (1) |notifier|, and (2)
842 | |sourceObservable|. We "unsubscribe" from **both** of them in the following situations:
843 |
844 | 1. |notifier| starts emitting values (either "next" or "error"). In this case, we
845 | unsubscribe from |notifier| since we got all we need from it, and no longer need it to
846 | keep producing values. We also unsubscribe from |sourceObservable|, because it no
847 | longer needs to produce values that get plumbed through this method's returned
848 | |observable|, because we're manually ending the subscription to |observable|, since
849 | |notifier| finally produced a value.
850 |
851 | 1. |sourceObservable| either {{Subscriber/error()}}s or {{Subscriber/complete()}}s
852 | itself. In this case, we unsubscribe from |notifier| since we no longer need to listen
853 | for values it emits in order to determine when |observable| can stop mirroring values
854 | from |sourceObservable| (since |sourceObservable| ran to completion by itself).
855 | Unsubscribing from |sourceObservable| isn't necessary, since its subscription has been
856 | exhausted by itself.
857 |
858 |
859 | 1. Let |notifierObserver| be a new [=internal observer=], initialized as follows:
860 |
861 | : [=internal observer/next steps=]
862 | :: Run |subscriber|'s {{Subscriber/complete()}} method.
863 |
864 | Note: This will "unsubscribe" from |sourceObservable|, if it has been subscribed to by
865 | this point. This is because |sourceObservable| is subscribed to with the "outer"
866 | |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=] as
867 | an input signal, and that signal will get [=AbortSignal/signal abort|aborted=] when the
868 | "outer" |subscriber|'s {{Subscriber/complete()}} is called above (and below).
869 |
870 | : [=internal observer/error steps=]
871 | :: Run |subscriber|'s {{Subscriber/complete()}} method.
872 |
873 | Note: We do not specify [=internal observer/complete steps=], because if the |notifier|
874 | {{Observable}} completes itself, we do not need to complete the |subscriber| associated
875 | with the |observable| returned from this method. Rather, the |observable| will continue to
876 | mirror |sourceObservable| uninterrupted.
877 |
878 | 1. Let |options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is
879 | |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=].
880 |
881 | 1.
Subscribe to |notifier| given
882 | |notifierObserver| and |options|.
883 |
884 | 1. If |subscriber|'s [=Subscriber/active=] is false, then return.
885 |
886 | Note: This means that |sourceObservable|'s [=Observable/subscribe callback=] will not even
887 | get invoked once, if |notifier| synchronously emits a value. If |notifier| only
888 | "completes" synchronously though (without emitting a "next" or "error" value), then
889 | |subscriber|'s [=Subscriber/active=] will still be true, and we proceed to subscribe to
890 | |sourceObservable|, which |observable| will mirror uninterrupted.
891 |
892 | 1. Let |sourceObserver| be a new [=internal observer=], initialized as follows:
893 |
894 | : [=internal observer/next steps=]
895 | :: Run |subscriber|'s {{Subscriber/next()}} method, given the passed in
value.
897 |
898 | : [=internal observer/error steps=]
899 | :: Run |subscriber|'s {{Subscriber/error()}} method, given the passed in
error.
901 |
902 | : [=internal observer/complete steps=]
903 | :: Run |subscriber|'s {{Subscriber/complete()}} method.
904 |
905 | Note: |sourceObserver| is mostly a pass-through, mirroring everything that
906 | |sourceObservable| emits, with the exception of having the ability to unsubscribe from the
907 | |notifier| {{Observable}} in the case where |sourceObservable| is exhausted before
908 | |notifier| emits anything.
909 |
910 | 1.
Subscribe to |sourceObservable|
911 | given |sourceObserver| and |options|.
912 |
913 | 1. Return |observable|.
914 |
915 |
916 | /dom/observable/tentative/observable-takeUntil.any.js
917 | /dom/observable/tentative/observable-takeUntil.window.js
918 |
919 |
920 |
921 |
922 | The
map(|mapper|) method steps are:
923 |
924 | 1. Let |sourceObservable| be [=this=].
925 |
926 | 1. Let |observable| be a [=new=] {{Observable}} whose [=Observable/subscribe callback=] is an
927 | algorithm that takes a {{Subscriber}} |subscriber| and does the following:
928 |
929 | 1. Let |idx| be an {{unsigned long long}}, initially 0.
930 |
931 | 1. Let |sourceObserver| be a new [=internal observer=], initialized as follows:
932 |
933 | : [=internal observer/next steps=]
934 | :: 1. [=Invoke=] |mapper| with «the passed in
value, |idx|» and
935 | "`rethrow`"; let |mappedValue| be the returned value.
936 |
937 | If
an exception |E| was thrown, then
938 | run |subscriber|'s {{Subscriber/error()}} method, given |E|, and abort these steps.
939 |
940 | 1. Increment |idx|.
941 |
942 | 1. Run |subscriber|'s {{Subscriber/next()}} method, given |mappedValue|.
943 |
944 | : [=internal observer/error steps=]
945 | :: Run |subscriber|'s {{Subscriber/error()}} method, given the passed in
error.
947 |
948 | : [=internal observer/complete steps=]
949 | :: Run |subscriber|'s {{Subscriber/complete()}} method.
950 |
951 | 1. Let |options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is
952 | |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=].
953 |
954 | 1.
Subscribe to |sourceObservable|
955 | given |sourceObserver| and |options|.
956 |
957 | 1. Return |observable|.
958 |
959 |
960 | /dom/observable/tentative/observable-map.any.js
961 | /dom/observable/tentative/observable-map.window.js
962 |
963 |
964 |
965 |
966 | The
filter(|predicate|) method steps are:
967 |
968 | 1. Let |sourceObservable| be [=this=].
969 |
970 | 1. Let |observable| be a [=new=] {{Observable}} whose [=Observable/subscribe callback=] is an
971 | algorithm that takes a {{Subscriber}} |subscriber| and does the following:
972 |
973 | 1. Let |idx| be an {{unsigned long long}}, initially 0.
974 |
975 | 1. Let |sourceObserver| be a new [=internal observer=], initialized as follows:
976 |
977 | : [=internal observer/next steps=]
978 | :: 1. [=Invoke=] |predicate| with «the passed in |value|, |idx|» and "`rethrow`"; let
979 | |matches| be the returned value.
980 |
981 | If
an exception |E| was thrown, then
982 | run |subscriber|'s {{Subscriber/error()}} method, given |E|, and abort these steps.
983 |
984 | 1. Set |idx| to |idx| + 1.
985 |
986 | 1. If |matches| is true, then run |subscriber|'s {{Subscriber/next()}} method, given
987 | |value|.
988 |
989 | : [=internal observer/error steps=]
990 | :: Run |subscriber|'s {{Subscriber/error()}} method, given the passed in
error.
992 |
993 | : [=internal observer/complete steps=]
994 | :: Run |subscriber|'s {{Subscriber/complete()}} method.
995 |
996 | 1. Let |options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is
997 | |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=].
998 |
999 | 1.
Subscribe to |sourceObservable|
1000 | given |sourceObserver| and |options|.
1001 |
1002 | 1. Return |observable|.
1003 |
1004 |
1005 | /dom/observable/tentative/observable-filter.any.js
1006 |
1007 |
1008 |
1009 |
1010 | The
take(|amount|) method steps are:
1011 |
1012 | 1. Let |sourceObservable| be [=this=].
1013 |
1014 | 1. Let |observable| be a [=new=] {{Observable}} whose [=Observable/subscribe callback=] is an
1015 | algorithm that takes a {{Subscriber}} |subscriber| and does the following:
1016 |
1017 | 1. Let |remaining| be |amount|.
1018 |
1019 | 1. If |remaining| is 0, then run |subscriber|'s {{Subscriber/complete()}} method and abort
1020 | these steps.
1021 |
1022 | 1. Let |sourceObserver| be a new [=internal observer=], initialized as follows:
1023 |
1024 | : [=internal observer/next steps=]
1025 | :: 1. Run |subscriber|'s {{Subscriber/next()}} method with the passed in
value.
1027 |
1028 | 1. Decrement |remaining|.
1029 |
1030 | 1. If |remaining| is 0, then run |subscriber|'s {{Subscriber/complete()}} method.
1031 |
1032 | : [=internal observer/error steps=]
1033 | :: Run |subscriber|'s {{Subscriber/error()}} method, given the passed in
error.
1035 |
1036 | : [=internal observer/complete steps=]
1037 | :: Run |subscriber|'s {{Subscriber/complete()}} method.
1038 |
1039 | 1. Let |options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is
1040 | |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=].
1041 |
1042 | 1.
Subscribe to |sourceObservable|
1043 | given |sourceObserver| and |options|.
1044 |
1045 | 1. Return |observable|.
1046 |
1047 |
1048 | /dom/observable/tentative/observable-take.any.js
1049 |
1050 |
1051 |
1052 |
1053 | The
drop(|amount|) method steps are:
1054 |
1055 | 1. Let |sourceObservable| be [=this=].
1056 |
1057 | 1. Let |observable| be a [=new=] {{Observable}} whose [=Observable/subscribe callback=] is an
1058 | algorithm that takes a {{Subscriber}} |subscriber| and does the following:
1059 |
1060 | 1. Let |remaining| be |amount|.
1061 |
1062 | 1. Let |sourceObserver| be a new [=internal observer=], initialized as follows:
1063 |
1064 | : [=internal observer/next steps=]
1065 | :: 1. If |remaining| is > 0, then decrement |remaining| and abort these steps.
1066 |
1067 | 1. [=Assert=]: |remaining| is 0.
1068 |
1069 | 1. Run |subscriber|'s {{Subscriber/next()}} method with the passed in
value.
1071 |
1072 | : [=internal observer/error steps=]
1073 | :: Run |subscriber|'s {{Subscriber/error()}} method, given the passed in
error.
1075 |
1076 | : [=internal observer/complete steps=]
1077 | :: Run |subscriber|'s {{Subscriber/complete()}} method.
1078 |
1079 | 1. Let |options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is
1080 | |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=].
1081 |
1082 | 1.
Subscribe to |sourceObservable|
1083 | given |sourceObserver| and |options|.
1084 |
1085 | 1. Return |observable|.
1086 |
1087 |
1088 | /dom/observable/tentative/observable-drop.any.js
1089 |
1090 |
1091 |
1092 |
1093 | The
flatMap(|mapper|) method steps are:
1094 |
1095 | 1. Let |sourceObservable| be [=this=].
1096 |
1097 | 1. Let |observable| be a [=new=] {{Observable}} whose [=Observable/subscribe callback=] is an
1098 | algorithm that takes a {{Subscriber}} |subscriber| and does the following:
1099 |
1100 | 1. Let |idx| be an {{unsigned long long}}, initially 0.
1101 |
1102 | 1. Let |outerSubscriptionHasCompleted| to a [=boolean=], initially false.
1103 |
1104 | 1. Let |queue| be a new [=list=] of {{any}} values, initially empty.
1105 |
1106 | Note: This |queue| is used to store any {{Observable}}s emitted by |sourceObservable|,
1107 | while |observable| is currently subscribed to an {{Observable}} emitted earlier by
1108 | |sourceObservable| that has not yet been exhausted.
1109 |
1110 | 1. Let |activeInnerSubscription| be a [=boolean=], initially false.
1111 |
1112 | 1. Let |sourceObserver| be a new [=internal observer=], initialized as follows:
1113 |
1114 | : [=internal observer/next steps=]
1115 | :: 1. If |activeInnerSubscription| is true, then:
1116 |
1117 | 1. [=list/Append=] |value| to |queue|.
1118 |
1119 | Note: This |value| will eventually be processed once the {{Observable}} that is
1120 | currently subscribed-to (as indicated by |activeInnerSubscription|) is exhausted.
1121 |
1122 | 1. Otherwise:
1123 |
1124 | 1. Set |activeInnerSubscription| to true.
1125 |
1126 | 1. Run the [=flatmap process next value steps=] with |value|, |subscriber|,
1127 | |mapper|, and
references to all of the following: |queue|,
1128 | |activeInnerSubscription|, |outerSubscriptionHasCompleted|, and |idx|.
1129 |
1130 |
1131 |
Note: This [=flatmap process next value steps=] will subscribe to the
1132 | {{Observable}} derived from |value| (if one such can be derived) and keep
1133 | processing values from it until its subscription becomes inactive (either by
1134 | error or completion). If this "inner" {{Observable}} completes, then the
1135 | processing steps will recursively invoke themselves with the next {{any}} in
1136 | |queue|.
1137 |
1138 |
If no such value [=list/exists=], then the processing steps will terminate,
1139 | unsetting |activeInnerSubscription|, so that future values emitted from
1140 | |sourceObservable| are processed correctly.
1141 |
1142 |
1143 | : [=internal observer/error steps=]
1144 | :: Run |subscriber|'s {{Subscriber/error()}} method, given the passed in
error.
1146 |
1147 | : [=internal observer/complete steps=]
1148 | :: 1. Set |outerSubscriptionHasCompleted| to true.
1149 |
1150 | Note: If |activeInnerSubscription| is true, then the below step will *not* complete
1151 | |subscriber|. In that case, the [=flatmap process next value steps=] will be
1152 | responsible for completing |subscriber| when |queue| is [=list/empty=], after the
1153 | "inner" subscription becomes inactive.
1154 |
1155 | 1. If |activeInnerSubscription| is false and |queue| is [=list/empty=], run
1156 | |subscriber|'s {{Subscriber/complete()}} method.
1157 |
1158 | 1. Let |options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is
1159 | |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=].
1160 |
1161 | 1.
Subscribe to |sourceObservable|
1162 | given |sourceObserver| and |options|.
1163 |
1164 | 1. Return |observable|.
1165 |
1166 |
1167 |
1168 | The
flatmap process next value steps, given an {{any}} |value|, a {{Subscriber}}
1169 | |subscriber|, a {{Mapper}} |mapper|, and
references to all of the following: a [=list=] of
1170 | {{any}} values |queue|, a [=boolean=] |activeInnerSubscription|, a [=boolean=]
1171 | |outerSubscriptionHasCompleted|, and an {{unsigned long long}} |idx|:
1172 |
1173 | 1. Let |mappedResult| be the result of [=invoking=] |mapper| with «|value|, |idx|» and
1174 | "`rethrow`".
1175 |
1176 | If
an exception |E| was thrown, then run
1177 | |subscriber|'s {{Subscriber/error()}} method, given |E|, and abort these steps.
1178 |
1179 | 1. Set |idx| to |idx| + 1.
1180 |
1181 | 1. Let |innerObservable| be the result of calling {{Observable/from()}} with |mappedResult|.
1182 |
1183 | If
an exception |E| was thrown, then run
1184 | |subscriber|'s {{Subscriber/error()}} method, given |E|, and abort these steps.
1185 |
1186 | Issue: We shouldn't invoke {{Observable/from()}} directly. Rather, we should call some
1187 | internal algorithm that passes-back the exceptions for us to handle properly here, since we
1188 | want to pipe them to |subscriber|.
1189 |
1190 | 1. Let |innerObserver| be a new [=internal observer=], initialized as follows:
1191 |
1192 | : [=internal observer/next steps=]
1193 | :: Run |subscriber|'s {{Subscriber/next()}} method, given the passed in |value|.
1194 |
1195 | : [=internal observer/error steps=]
1196 | :: Run |subscriber|'s {{Subscriber/error()}} method, given the passed in
error.
1198 |
1199 | : [=internal observer/complete steps=]
1200 | :: 1. If |queue| is not empty, then:
1201 |
1202 | 1. Let |nextValue| be the first item in |queue|; [=list/remove=] remove this item from
1203 | |queue|.
1204 |
1205 | 1. Run [=flatmap process next value steps=] given |nextValue|, |subscriber|, |mapper|,
1206 | and
references to |queue| and |activeInnerSubscription|.
1207 |
1208 | 1. Otherwise:
1209 |
1210 | 1. Set |activeInnerSubscription| to false.
1211 |
1212 | Note: Because |activeInnerSubscription| is a reference, this has the effect of
1213 | ensuring that all subsequent values emitted from the "outer" {{Observable}} (called
1214 |
sourceObservable.
1215 |
1216 | 1. If |outerSubscriptionHasCompleted| is true, run |subscriber|'s
1217 | {{Subscriber/complete()}} method.
1218 |
1219 | Note: This means the "outer" {{Observable}} has already completed, but did not
1220 | proceed to complete |subscriber| yet because there was at least one more pending
1221 | "inner" {{Observable}} (i.e., |innerObservable|) that had already been queued and
1222 | had not yet completed. Until right now!
1223 |
1224 | 1. Let |innerOptions| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is
1225 | |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=].
1226 |
1227 | 1.
Subscribe to |innerObservable| given
1228 | |innerObserver| and |innerOptions|.
1229 |
1230 |
1231 |
1232 | The
switchMap(|mapper|) method steps are:
1233 |
1234 | 1. Let |sourceObservable| be [=this=].
1235 |
1236 | 1. Let |observable| be a [=new=] {{Observable}} whose [=Observable/subscribe callback=] is an
1237 | algorithm that takes a {{Subscriber}} |subscriber| and does the following:
1238 |
1239 | 1. Let |idx| be an {{unsigned long long}}, initially 0.
1240 |
1241 | 1. Let |outerSubscriptionHasCompleted| be a [=boolean=], initially false.
1242 |
1243 | 1. Let |activeInnerAbortController| be an {{AbortController}}-or-null, initially null.
1244 |
1245 | Note: This {{AbortController}} is assigned to a new {{AbortController}} only by this
1246 | algorithm's
next steps (below), and only assigned to
1247 | null by the [=switchmap process next value steps=], when the "inner" {{Observable}} either
1248 | completes or errors. This variable is used as a marker for whether there is currently an
1249 | active "inner" subscription. The
complete steps
1250 | below care about this, because if |sourceObservable| completes while there is an active
1251 | "inner" subscription, we do not immediately complete |subscriber|. In that case,
1252 | |subscriber|'s completion becomes blocked on the "inner" subscription's completion.
1253 |
1254 | 1. Let |sourceObserver| be a new [=internal observer=], initialized as follows:
1255 |
1256 | :
[=internal observer/next steps=]
1257 | :: 1. If |activeInnerAbortController| is not null, then [=AbortController/signal abort=]
1258 | |activeInnerAbortController|.
1259 |
1260 | Note: This "unsubscribes" from the "inner" {{Observable}} that was derived from the
1261 | value that was
last pushed from |sourceObservable|. Then we immediately
1262 | subscribe to the
new {{Observable}} that we're about to derive from |value|,
1263 | i.e., the
most-recently pushed value from |sourceObservable|.
1264 |
1265 | 1. Set |activeInnerAbortController| to a [=new=] {{AbortController}}.
1266 |
1267 | 1. Run the [=switchmap process next value steps=] with |value|, |subscriber|, |mapper|,
1268 | and
references to all of the following: |activeInnerAbortController|,
1269 | |outerSubscriptionHasCompleted|, and |idx|.
1270 |
1271 | Note: The [=switchmap process next value steps=] will subscribe to the
1272 | {{Observable}} derived from |value| (if one such can be derived) and keep processing
1273 | values from it until either (1) its subscription becomes inactive (either by error
1274 | or completion), or (2) |activeInnerAbortController|
gets aborted, due to |sourceObservable| pushing
1276 | another
newer value that will replace the current "inner" subscription.
1277 |
1278 | : [=internal observer/error steps=]
1279 | :: Run |subscriber|'s {{Subscriber/error()}} method, given the passed in
error.
1281 |
1282 | :
[=internal observer/complete steps=]
1283 | :: 1. Set |outerSubscriptionHasCompleted| to true.
1284 |
1285 | Note: If |activeInnerAbortController| is not null, then we don't immediately
1286 | complete |subscriber|. Instead, the [=switchmap process next value steps=] will
1287 | complete |subscriber| when the inner subscription finally completes itself.
1288 |
1289 | 1. If |activeInnerAbortController| is null, run |subscriber|'s
1290 | {{Subscriber/complete()}} method.
1291 |
1292 | 1. Let |options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is
1293 | |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=].
1294 |
1295 | 1.
Subscribe to |sourceObservable|
1296 | given |sourceObserver| and |options|.
1297 |
1298 | 1. Return |observable|.
1299 |
1300 |
1301 |
1302 | The
switchmap process next value steps, given an {{any}} |value|, a {{Subscriber}}
1303 | |subscriber|, a {{Mapper}} |mapper|, and
references to all of the following: an
1304 | {{AbortController}} |activeInnerAbortController|, a [=boolean=] |outerSubscriptionHasCompleted|,
1305 | and an {{unsigned long long}} |idx| are to run these steps:
1306 |
1307 | 1. Let |mappedResult| be the result of [=invoking=] |mapper| with «|value|, |idx|» and
1308 | "`rethrow`".
1309 |
1310 | If
an exception |E| was thrown, then run
1311 | |subscriber|'s {{Subscriber/error()}} method, given |E|, and abort these steps.
1312 |
1313 | 1. Set |idx| to |idx| + 1.
1314 |
1315 | 1. Let |innerObservable| be the result of calling {{Observable/from()}} with |mappedResult|.
1316 |
1317 | If
an exception |E| was thrown, then run
1318 | |subscriber|'s {{Subscriber/error()}} method, given |E|, and abort these steps.
1319 |
1320 | 1. Let |innerObserver| be a new [=internal observer=], initialized as follows:
1321 |
1322 | : [=internal observer/next steps=]
1323 | :: Run |subscriber|'s {{Subscriber/next()}} method, given the passed in |value|.
1324 |
1325 | : [=internal observer/error steps=]
1326 | :: Run |subscriber|'s {{Subscriber/error()}} method, given the passed in
error.
1328 |
1329 | Note: We don't have to set |activeInnerAbortController| to null here, to signal to the
1330 | {{Observable/switchMap()}} method steps above that the inner "subscription" has been
1331 | canceled. That's because calling |subscriber|'s {{Subscriber/error()}} method already
1332 | unsubscribes from the "outer" source Observable, so it will not be able to push any more
1333 | values to the {{Observable/switchMap()}} internal observer.
1334 |
1335 | : [=internal observer/complete steps=]
1336 | :: 1. If |outerSubscriptionHasCompleted| is true, run |subscriber|'s
1337 | {{Subscriber/complete()}} method.
1338 |
1339 | 1. Otherwise, set |activeInnerAbortController| to null.
1340 |
1341 | Note: Because this variable is a reference, it signals to the
switchMap complete steps that there is no active
1343 | inner subscription.
1344 |
1345 | 1. Let |innerOptions| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is the
1346 | result of [=creating a dependent abort signal=] from the list
1347 | «|activeInnerAbortController|'s [=AbortController/signal=], |subscriber|'s
1348 | [=Subscriber/subscription controller=]'s [=AbortController/signal=]», using {{AbortSignal}},
1349 | and the [=current realm=].
1350 |
1351 | 1.
Subscribe to |innerObservable| given
1352 | |innerObserver| and |innerOptions|.
1353 |
1354 |
1355 |
1356 | The
inspect(|inspectorUnion|) method steps are:
1357 |
1358 | 1. Let |subscribe callback| be a {{VoidFunction}}-or-null, initially null.
1359 |
1360 | 1. Let |next callback| be a {{ObservableSubscriptionCallback}}-or-null, initially null.
1361 |
1362 | 1. Let |error callback| be a {{ObservableSubscriptionCallback}}-or-null, initially null.
1363 |
1364 | 1. Let |complete callback| be a {{VoidFunction}}-or-null, initially null.
1365 |
1366 | 1. Let |abort callback| be a {{ObservableInspectorAbortHandler}}-or-null, initially null.
1367 |
1368 | 1. Process |inspectorUnion| as follows:
1369 |
1370 | - If |inspectorUnion| is an {{ObservableSubscriptionCallback}}
1371 | -
1372 | 1. Set |next callback| to |inspectorUnion|.
1373 |
1374 |
- If |inspectorUnion| is an {{ObservableInspector}}
1375 | -
1376 | 1. If {{ObservableInspector/subscribe}} [=map/exists=] in |inspectorUnion|, then set
1377 | |subscribe callback| to it.
1378 |
1379 | 1. If {{ObservableInspector/next}} [=map/exists=] in |inspectorUnion|, then set |next
1380 | callback| to it.
1381 |
1382 | 1. If {{ObservableInspector/error}} [=map/exists=] in |inspectorUnion|, then set |error
1383 | callback| to it.
1384 |
1385 | 1. If {{ObservableInspector/complete}} [=map/exists=] in |inspectorUnion|, then set
1386 | |complete callback| to it.
1387 |
1388 | 1. If {{ObservableInspector/abort}} [=map/exists=] in |inspectorUnion|, then set |abort
1389 | callback| to it.
1390 |
1391 |
1392 |
1393 | 1. Let |sourceObservable| be [=this=].
1394 |
1395 | 1. Let |observable| be a [=new=] {{Observable}} whose [=Observable/subscribe callback=] is an
1396 | algorithm that takes a {{Subscriber}} |subscriber| and does the following:
1397 |
1398 | 1. If |subscribe callback| is not null, then [=invoke=] it with «» and "`rethrow`".
1399 |
1400 | If
an exception |E| was thrown, then run
1401 | |subscriber|'s {{Subscriber/error()}} method, given |E|, and abort these steps.
1402 |
1403 | Note: The result of this is that |sourceObservable| is never subscribed to.
1404 |
1405 | 1. If |abort callback| is not null, then [=AbortSignal/add|add the following abort
1406 | algorithm=] to |subscriber|'s [=Subscriber/subscription controller=]'s
1407 | [=AbortController/signal=]:
1408 |
1409 | 1. [=Invoke=] |abort callback| with «|subscriber|'s [=Subscriber/subscription
1410 | controller=]'s [=AbortController/signal=]'s [=AbortSignal/abort reason=]» and "`report`".
1411 |
1412 | 1. Let |sourceObserver| be a new [=internal observer=], initialized as follows:
1413 |
1414 | : [=internal observer/next steps=]
1415 | :: 1. If |next callback| is not null, then [=invoke=] |next callback| with «the passed in
1416 | |value|» and "`rethrow`".
1417 |
1418 | If
an exception |E| was thrown,
1419 | then:
1420 |
1421 | 1. [=AbortSignal/Remove=] |abort callback| from |subscriber|'s
1422 | [=Subscriber/subscription controller=]'s [=AbortController/signal=].
1423 |
1424 | Note: This step is important, because the |abort callback| is only meant to be
1425 | called for *consumer-initiated* unsubscriptions. When the producer terminates
1426 | the subscription (via |subscriber|'s {{Subscriber/error()}} or
1427 | {{Subscriber/complete()}} methods) like below, we have to ensure that |abort
1428 | callback| is not run.
1429 |
1430 | Issue: This matches Chromium's implementation, but consider holding a reference
1431 | to the originally-passed-in {{SubscribeOptions}}'s {{SubscribeOptions/signal}}
1432 | and just invoking |abort callback| when *it* aborts. The result is likely the
1433 | same, but needs investigation.
1434 |
1435 | 1. Run |subscriber|'s {{Subscriber/error()}} method, given |E|, and abort these
1436 | steps.
1437 |
1438 | 1. Run |subscriber|'s {{Subscriber/next()}} method with the passed in |value|.
1439 |
1440 | : [=internal observer/error steps=]
1441 | :: 1. [=AbortSignal/Remove=] |abort callback| from |subscriber|'s
1442 | [=Subscriber/subscription controller=]'s [=AbortController/signal=].
1443 |
1444 | 1. If |error callback| is not null, then [=invoke=] |error callback| with «the passed
1445 | in |error|» and "`rethrow`".
1446 |
1447 | If
an exception |E| was thrown, then
1448 | run |subscriber|'s {{Subscriber/error()}} method, given |E|, and abort these steps.
1449 |
1450 | 1. Run |subscriber|'s {{Subscriber/error()}} method, given the passed in |error|.
1451 |
1452 | : [=internal observer/complete steps=]
1453 | :: 1. [=AbortSignal/Remove=] |abort callback| from |subscriber|'s
1454 | [=Subscriber/subscription controller=]'s [=AbortController/signal=].
1455 |
1456 | 1. If |complete callback| is not null, then [=invoke=] |complete callback| with «»
1457 | and "`rethrow`".
1458 |
1459 | If
an exception |E| was thrown, then
1460 | run |subscriber|'s {{Subscriber/error()}} method, given |E|, and abort these steps.
1461 |
1462 | 1. Run |subscriber|'s {{Subscriber/complete()}} method.
1463 |
1464 | 1. Let |options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is
1465 | |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=].
1466 |
1467 | 1.
Subscribe to |sourceObservable|
1468 | given |sourceObserver| and |options|.
1469 |
1470 | 1. Return |observable|.
1471 |
1472 |
1473 | /dom/observable/tentative/observable-inspect.any.js
1474 |
1475 |
1476 |
1477 |
1478 | The
catch(|callback|) method steps are:
1479 |
1480 | 1. Let |sourceObservable| be [=this=].
1481 |
1482 | 1. Let |observable| be a [=new=] {{Observable}} whose [=Observable/subscribe callback=] is an
1483 | algorithm that takes a {{Subscriber}} |subscriber| and does the following:
1484 |
1485 | 1. Let |sourceObserver| be a new [=internal observer=], initialized as follows:
1486 |
1487 | : [=internal observer/next steps=]
1488 | :: Run |subscriber|'s {{Subscriber/next()}} method, given the passed in
value.
1490 |
1491 | : [=internal observer/error steps=]
1492 | ::
1493 | 1. [=Invoke=] |callback| with «the passed in
error» and "`rethrow`".
1494 | Let |result| be the returned value.
1495 |
1496 | If
an exception |E| was thrown, then
1497 | run |subscriber|'s {{Subscriber/error()}} with |E|, and abort these steps.
1498 |
1499 | 1. Let |innerObservable| be the result of calling {{Observable/from()}} with |result|.
1500 |
1501 | If
an exception |E| was thrown, then
1502 | run |subscriber|'s {{Subscriber/error()}} method, given |E|, and abort these steps.
1503 |
1504 | Issue: We shouldn't invoke {{Observable/from()}} directly. Rather, we should call
1505 | some internal algorithm that passes-back the exceptions for us to handle properly
1506 | here, since we want to pipe them to |subscriber|.
1507 |
1508 | 1. Let |innerObserver| be a new [=internal observer=], initialized as follows:
1509 |
1510 | : [=internal observer/next steps=]
1511 | :: Run |subscriber|'s {{Subscriber/next()}} method, given the passed in
value.
1513 |
1514 | : [=internal observer/error steps=]
1515 | :: Run |subscriber|'s {{Subscriber/error()}} method, given the passed in
error.
1517 |
1518 | : [=internal observer/complete steps=]
1519 | :: Run |subscriber|'s {{Subscriber/complete()}} method.
1520 |
1521 | 1. Let |innerOptions| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}}
1522 | is |subscriber|'s [=Subscriber/subscription controller=]'s
1523 | [=AbortController/signal=].
1524 |
1525 | 1.
Subscribe to |innerObservable|
1526 | given |innerObserver| and |innerOptions|.
1527 |
1528 | Note: We're free to subscribe to |innerObservable| here without first
1529 | "unsubscribing" from |sourceObservable|, and without fear that |sourceObservable|
1530 | will keep emitting values, because all of this is happening inside of the [=internal
1531 | observer/error steps=] associated with |sourceObservable|. This means
1532 | |sourceObservable| has already completed its subscription and will no longer produce
1533 | any values, and we are free to safely switch our source of values to
1534 | |innerObservable|.
1535 |
1536 | : [=internal observer/complete steps=]
1537 | :: Run |subscriber|'s {{Subscriber/complete()}} method.
1538 |
1539 | 1. Let |options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is
1540 | |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=].
1541 |
1542 | 1.
Subscribe to |sourceObservable|
1543 | given |sourceObserver| and |options|.
1544 |
1545 | 1. Return |observable|.
1546 |
1547 |
1548 |
1549 | The
finally(|callback|) method steps are:
1550 |
1551 | 1. Let |sourceObservable| be [=this=].
1552 |
1553 | 1. Let |observable| be a [=new=] {{Observable}} whose [=Observable/subscribe callback=] is an
1554 | algorithm that takes a {{Subscriber}} |subscriber| and does the following:
1555 |
1556 | 1. Run |subscriber|'s {{Subscriber/addTeardown()}} method with |callback|.
1557 |
1558 | 1. Let |sourceObserver| be a new [=internal observer=], initialized as follows:
1559 |
1560 | : [=internal observer/next steps=]
1561 | :: Run |subscriber|'s {{Subscriber/next()}} method, given the passed in
value.
1563 |
1564 | : [=internal observer/error steps=]
1565 | :: 1. Run |subscriber|'s {{Subscriber/error()}} method, given the passed in
error.
1567 |
1568 | : [=internal observer/complete steps=]
1569 | :: 1. Run |subscriber|'s {{Subscriber/complete()}} method.
1570 |
1571 | 1. Let |options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is
1572 | |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=].
1573 |
1574 | 1.
Subscribe to |sourceObservable|
1575 | given |sourceObserver| and |options|.
1576 |
1577 | 1. Return |observable|.
1578 |
1579 |
1580 |
1581 | {{Promise}}-returning operators
1582 |
1583 |
1584 | The
toArray(|options|) method steps are:
1585 |
1586 | 1. Let |p| [=a new promise=].
1587 |
1588 | 1. If |options|'s {{SubscribeOptions/signal}} is not null:
1589 |
1590 | 1. If |options|'s {{SubscribeOptions/signal}} is [=AbortSignal/aborted=], then:
1591 |
1592 | 1. [=Reject=] |p| with |options|'s {{SubscribeOptions/signal}}'s [=AbortSignal/abort
1593 | reason=].
1594 |
1595 | 1. Return |p|.
1596 |
1597 | 1. [=AbortSignal/add|Add the following abort algorithm=] to |options|'s
1598 | {{SubscribeOptions/signal}}:
1599 |
1600 | 1. [=Reject=] |p| with |options|'s {{SubscribeOptions/signal}}'s [=AbortSignal/abort
1601 | reason=].
1602 |
1603 | Note: All we have to do here is [=reject=] |p|. Note that the subscription to [=this=]
1604 | {{Observable}} will also be closed automatically, since the "inner" Subscriber gets
1605 | [=close a subscription|closed=] in response to |options|'s {{SubscribeOptions/signal}}
1606 | getting [=AbortSignal/signal abort=].
1607 |
1608 | 1. Let |values| be a new [=list=].
1609 |
1610 | 1. Let |observer| be a new [=internal observer=], initialized as follows:
1611 |
1612 | : [=internal observer/next steps=]
1613 | :: [=list/Append=] the passed in
value to |values|.
1614 |
1615 | : [=internal observer/error steps=]
1616 | :: [=Reject=] |p| with the passed in
error.
1617 |
1618 | : [=internal observer/complete steps=]
1619 | :: [=Resolve=] |p| with |values|.
1620 |
1621 | 1.
Subscribe to [=this=] given |observer|
1622 | and |options|.
1623 |
1624 | 1. Return |p|.
1625 |
1626 |
1627 | /dom/observable/tentative/observable-toArray.any.js
1628 |
1629 |
1630 |
1631 |
1632 | The
forEach(|callback|, |options|) method steps are:
1633 |
1634 | 1. Let |p| [=a new promise=].
1635 |
1636 | 1. Let |visitor callback controller| be a [=new=] {{AbortController}}.
1637 |
1638 | 1. Let |internal options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is the
1639 | result of [=creating a dependent abort signal=] from the list «|visitor callback
1640 | controller|'s [=AbortController/signal=], |options|'s {{SubscribeOptions/signal}} if
1641 | non-null», using {{AbortSignal}}, and the [=current realm=].
1642 |
1643 |
1644 |
Many trivial [=internal observers=] act as pass-throughs, and do not control the
1645 | subscription to the {{Observable}} that they represent; that is, their [=internal
1646 | observer/error steps=] and [=internal observer/complete steps=] are called when the
1647 | subscription is terminated, and their [=internal observer/next steps=] simply pass some
1648 | version of the given value along the chain.
1649 |
1650 |
For this operator, however, the below |observer|'s [=internal observer/next steps=] are
1651 | responsible for actually aborting the underlying subscription to [=this=], in the event
1652 | that |callback| throws an exception. In that case, the {{SubscribeOptions}}'s
1653 | {{SubscribeOptions/signal}} we pass through to "Subscribe to an Observable", needs to be a [=creating a
1655 | dependent abort signal|dependent signal=] derived from |options|'s
1656 | {{SubscribeOptions/signal}}, **and** the {{AbortSignal}} of an {{AbortController}} that the
1657 | [=internal observer/next steps=] below has access to, and can [=AbortController/signal
1658 | abort=] when needed.
1659 |
1660 |
1661 | 1. If |internal options|'s {{SubscribeOptions/signal}} is [=AbortSignal/aborted=], then:
1662 |
1663 | 1. [=Reject=] |p| with |internal options|'s {{SubscribeOptions/signal}}'s [=AbortSignal/abort
1664 | reason=].
1665 |
1666 | 1. Return |p|.
1667 |
1668 | 1. [=AbortSignal/add|Add the following abort algorithm=] to |internal options|'s
1669 | {{SubscribeOptions/signal}}:
1670 |
1671 | 1. [=Reject=] |p| with |internal options|'s {{SubscribeOptions/signal}}'s [=AbortSignal/abort
1672 | reason=].
1673 |
1674 | Note: The fact that rejection of |p| is tied to |internal options|'s
1675 | {{SubscribeOptions/signal}}, and not |options|'s {{SubscribeOptions/signal}} means, that
1676 | any [=microtasks=] [=queue a microtask|queued=] during the firing of |options|'s
1677 | {{SubscribeOptions/signal}}'s {{AbortSignal/abort}} event will run before |p|'s rejection
1678 | handler runs.
1679 |
1680 | 1. Let |idx| be an {{unsigned long long}}, initially 0.
1681 |
1682 | 1. Let |observer| be a new [=internal observer=], initialized as follows:
1683 |
1684 | : [=internal observer/next steps=]
1685 | ::
1686 | 1. [=Invoke=] |callback| with «the passed in
value, |idx|» and
1687 | "`rethrow`".
1688 |
1689 | If
an exception |E| was thrown, then
1690 | [=reject=] |p| with |E|, and [=AbortController/signal abort=] |visitor callback
1691 | controller| with |E|.
1692 |
1693 | 1. Increment |idx|.
1694 |
1695 | : [=internal observer/error steps=]
1696 | :: [=Reject=] |p| with the passed in
error.
1697 |
1698 | : [=internal observer/complete steps=]
1699 | :: [=Resolve=] |p| with {{undefined}}.
1700 |
1701 | 1.
Subscribe to [=this=] given |observer|
1702 | and |internal options|.
1703 |
1704 | 1. Return |p|.
1705 |
1706 |
1707 | /dom/observable/tentative/observable-forEach.any.js
1708 |
1709 |
1710 |
1711 |
1712 | The
every(|predicate|, |options|) method steps are:
1713 |
1714 | 1. Let |p| [=a new promise=].
1715 |
1716 | 1. Let |controller| be a [=new=] {{AbortController}}.
1717 |
1718 | 1. Let |internal options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is the
1719 | result of [=creating a dependent abort signal=] from the list «|controller|'s
1720 | [=AbortController/signal=], |options|'s {{SubscribeOptions/signal}} if non-null», using
1721 | {{AbortSignal}}, and the [=current realm=].
1722 |
1723 | 1. If |internal options|'s {{SubscribeOptions/signal}} is [=AbortSignal/aborted=], then:
1724 |
1725 | 1. [=Reject=] |p| with |internal options|'s {{SubscribeOptions/signal}}'s [=AbortSignal/abort
1726 | reason=].
1727 |
1728 | 1. Return |p|.
1729 |
1730 | 1. [=AbortSignal/add|Add the following abort algorithm=] to |internal options|'s
1731 | {{SubscribeOptions/signal}}:
1732 |
1733 | 1. [=Reject=] |p| with |internal options|'s {{SubscribeOptions/signal}}'s [=AbortSignal/abort
1734 | reason=].
1735 |
1736 | 1. Let |idx| be an {{unsigned long long}}, initially 0.
1737 |
1738 | 1. Let |observer| be a new [=internal observer=], initialized as follows:
1739 |
1740 | : [=internal observer/next steps=]
1741 | :: 1. [=Invoke=] |predicate| with «the passed in
value, |idx|» and
1742 | "`rethrow`"; let |passed| be the returned value.
1743 |
1744 | If
an exception |E| was thrown, then
1745 | [=reject=] |p| with |E|, and [=AbortController/signal abort=] |controller| with |E|.
1746 |
1747 | 1. Set |idx| to |idx| + 1.
1748 |
1749 | 1. If |passed| is false, then [=resolve=] |p| with false, and [=AbortController/signal
1750 | abort=] |controller|.
1751 |
1752 | : [=internal observer/error steps=]
1753 | :: [=Reject=] |p| with the passed in
error.
1754 |
1755 | : [=internal observer/complete steps=]
1756 | :: [=Resolve=] |p| with true.
1757 |
1758 | 1.
Subscribe to [=this=] given |observer|
1759 | and |internal options|.
1760 |
1761 | 1. Return |p|.
1762 |
1763 |
1764 |
1765 | The
first(|options|) method steps are:
1766 |
1767 | 1. Let |p| [=a new promise=].
1768 |
1769 | 1. Let |controller| be a [=new=] {{AbortController}}.
1770 |
1771 | 1. Let |internal options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is the
1772 | result of [=creating a dependent abort signal=] from the list «|controller|'s
1773 | [=AbortController/signal=], |options|'s {{SubscribeOptions/signal}} if non-null», using
1774 | {{AbortSignal}}, and the [=current realm=].
1775 |
1776 | 1. If |internal options|'s {{SubscribeOptions/signal}} is [=AbortSignal/aborted=], then:
1777 |
1778 | 1. [=Reject=] |p| with |internal options|'s {{SubscribeOptions/signal}}'s [=AbortSignal/abort
1779 | reason=].
1780 |
1781 | 1. Return |p|.
1782 |
1783 | 1. [=AbortSignal/add|Add the following abort algorithm=] to |internal options|'s
1784 | {{SubscribeOptions/signal}}:
1785 |
1786 | 1. [=Reject=] |p| with |internal options|'s {{SubscribeOptions/signal}}'s [=AbortSignal/abort
1787 | reason=].
1788 |
1789 | 1. Let |internal observer| be a new [=internal observer=], initialized as follows:
1790 |
1791 | : [=internal observer/next steps=]
1792 | :: 1. [=Resolve=] |p| with the passed in
value.
1793 |
1794 | 1. [=AbortController/Signal abort=] |controller|.
1795 |
1796 | : [=internal observer/error steps=]
1797 | :: [=Reject=] |p| with the passed in
error.
1798 |
1799 | : [=internal observer/complete steps=]
1800 | :: [=Reject=] |p| with a new {{RangeError}}.
1801 |
1802 | Note: This is only reached when the source {{Observable}} completes *before* it emits a
1803 | single value.
1804 |
1805 | 1.
Subscribe to [=this=] given |internal
1806 | observer| and |internal options|.
1807 |
1808 | 1. Return |p|.
1809 |
1810 |
1811 |
1812 |
1813 | The
last(|options|) method steps are:
1814 |
1815 | 1. Let |p| [=a new promise=].
1816 |
1817 | 1. If |options|'s {{SubscribeOptions/signal}} is not null:
1818 |
1819 | 1. If |options|'s {{SubscribeOptions/signal}} is [=AbortSignal/aborted=], then:
1820 |
1821 | 1. [=Reject=] |p| with |options|'s {{SubscribeOptions/signal}}'s [=AbortSignal/abort
1822 | reason=].
1823 |
1824 | 1. Return |p|.
1825 |
1826 | 1. [=AbortSignal/add|Add the following abort algorithm=] to |options|'s
1827 | {{SubscribeOptions/signal}}:
1828 |
1829 | 1. [=Reject=] |p| with |options|'s {{SubscribeOptions/signal}}'s [=AbortSignal/abort
1830 | reason=].
1831 |
1832 | 1. Let |lastValue| be an {{any}}-or-null, initially null.
1833 |
1834 | 1. Let |hasLastValue| be a [=boolean=], initially false.
1835 |
1836 | 1. Let |observer| be a new [=internal observer=], initialized as follows:
1837 |
1838 | : [=internal observer/next steps=]
1839 | :: 1. Set |hasLastValue| to true.
1840 |
1841 | 1. Set |lastValue| to the passed in
value.
1842 |
1843 | : [=internal observer/error steps=]
1844 | :: [=Reject=] |p| with the passed in
error.
1845 |
1846 | : [=internal observer/complete steps=]
1847 | :: 1. If |hasLastValue| is true, [=resolve=] |p| with |lastValue|.
1848 |
1849 | 1. Otherwise, [=reject=] |p| with a new {{RangeError}}.
1850 |
1851 | Note: See the note in {{Observable/first()}}.
1852 |
1853 | 1.
Subscribe to [=this=] given |observer|
1854 | and |options|.
1855 |
1856 | 1. Return |p|.
1857 |
1858 |
1859 |
1860 | The
find(|predicate|, |options|) method steps are:
1861 |
1862 | 1. Let |p| [=a new promise=].
1863 |
1864 | 1. Let |controller| be a [=new=] {{AbortController}}.
1865 |
1866 | 1. Let |internal options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is the
1867 | result of [=creating a dependent abort signal=] from the list «|controller|'s
1868 | [=AbortController/signal=], |options|'s {{SubscribeOptions/signal}} if non-null», using
1869 | {{AbortSignal}}, and the [=current realm=].
1870 |
1871 | 1. If |internal options|'s {{SubscribeOptions/signal}} is [=AbortSignal/aborted=], then:
1872 |
1873 | 1. [=Reject=] |p| with |internal options|'s {{SubscribeOptions/signal}}'s [=AbortSignal/abort
1874 | reason=].
1875 |
1876 | 1. Return |p|.
1877 |
1878 | 1. [=AbortSignal/add|Add the following abort algorithm=] to |internal options|'s
1879 | {{SubscribeOptions/signal}}:
1880 |
1881 | 1. [=Reject=] |p| with |internal options|'s {{SubscribeOptions/signal}}'s [=AbortSignal/abort
1882 | reason=].
1883 |
1884 | 1. Let |idx| be an {{unsigned long long}}, initially 0.
1885 |
1886 | 1. Let |observer| be a new [=internal observer=], initialized as follows:
1887 |
1888 | : [=internal observer/next steps=]
1889 | :: 1. [=Invoke=] |predicate| with «the passed in |value|, |idx|» and "`rethrow`"; let
1890 | |passed| be the returned value.
1891 |
1892 | If
an exception |E| was thrown, then
1893 | [=reject=] |p| with |E|, and [=AbortController/signal abort=] |controller| with |E|.
1894 |
1895 | 1. Set |idx| to |idx| + 1.
1896 |
1897 | 1. If |passed| is true, then [=resolve=] |p| with |value|, and [=AbortController/signal
1898 | abort=] |controller|.
1899 |
1900 | : [=internal observer/error steps=]
1901 | :: [=Reject=] |p| with the passed in
error.
1902 |
1903 | : [=internal observer/complete steps=]
1904 | :: [=Resolve=] |p| with {{undefined}}.
1905 |
1906 | 1.
Subscribe to [=this=] given |observer|
1907 | and |internal options|.
1908 |
1909 | 1. Return |p|.
1910 |
1911 |
1912 |
1913 | The
some(|predicate|, |options|) method steps are:
1914 |
1915 | 1. Let |p| [=a new promise=].
1916 |
1917 | 1. Let |controller| be a [=new=] {{AbortController}}.
1918 |
1919 | 1. Let |internal options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is the
1920 | result of [=creating a dependent abort signal=] from the list «|controller|'s
1921 | [=AbortController/signal=], |options|'s {{SubscribeOptions/signal}} if non-null», using
1922 | {{AbortSignal}}, and the [=current realm=].
1923 |
1924 | 1. If |internal options|'s {{SubscribeOptions/signal}} is [=AbortSignal/aborted=], then:
1925 |
1926 | 1. [=Reject=] |p| with |internal options|'s {{SubscribeOptions/signal}}'s [=AbortSignal/abort
1927 | reason=].
1928 |
1929 | 1. Return |p|.
1930 |
1931 | 1. [=AbortSignal/add|Add the following abort algorithm=] to |internal options|'s
1932 | {{SubscribeOptions/signal}}:
1933 |
1934 | 1. [=Reject=] |p| with |internal options|'s {{SubscribeOptions/signal}}'s [=AbortSignal/abort
1935 | reason=].
1936 |
1937 | 1. Let |idx| be an {{unsigned long long}}, initially 0.
1938 |
1939 | 1. Let |observer| be a new [=internal observer=], initialized as follows:
1940 |
1941 | : [=internal observer/next steps=]
1942 | :: 1. [=Invoke=] |predicate| with «the passed in
value, |idx|» and
1943 | "`rethrow`"; let |passed| be the returned value.
1944 |
1945 | If
an exception |E| was thrown, then
1946 | [=reject=] |p| with |E|, and [=AbortController/signal abort=] |controller| with |E|.
1947 |
1948 | 1. Set |idx| to |idx| + 1.
1949 |
1950 | 1. If |passed| is true, then [=resolve=] |p| with true, and [=AbortController/signal
1951 | abort=] |controller|.
1952 |
1953 | : [=internal observer/error steps=]
1954 | :: [=Reject=] |p| with the passed in
error.
1955 |
1956 | : [=internal observer/complete steps=]
1957 | :: [=Resolve=] |p| with false.
1958 |
1959 | 1.
Subscribe to [=this=] given |observer|
1960 | and |internal options|.
1961 |
1962 | 1. Return |p|.
1963 |
1964 |
1965 |
1966 | The
reduce(|reducer|, |initialValue|, |options|)
1967 | method steps are:
1968 |
1969 | 1. Let |p| [=a new promise=].
1970 |
1971 | 1. Let |controller| be a [=new=] {{AbortController}}.
1972 |
1973 | 1. Let |internal options| be a new {{SubscribeOptions}} whose {{SubscribeOptions/signal}} is the
1974 | result of [=creating a dependent abort signal=] from the list «|controller|'s
1975 | [=AbortController/signal=], |options|'s {{SubscribeOptions/signal}} if non-null», using
1976 | {{AbortSignal}}, and the [=current realm=].
1977 |
1978 | 1. If |internal options|'s {{SubscribeOptions/signal}} is [=AbortSignal/aborted=], then:
1979 |
1980 | 1. [=Reject=] |p| with |internal options|'s {{SubscribeOptions/signal}}'s [=AbortSignal/abort
1981 | reason=].
1982 |
1983 | 1. Return |p|.
1984 |
1985 | 1. [=AbortSignal/add|Add the following abort algorithm=] to |internal options|'s
1986 | {{SubscribeOptions/signal}}:
1987 |
1988 | 1. [=Reject=] |p| with |internal options|'s {{SubscribeOptions/signal}}'s [=AbortSignal/abort
1989 | reason=].
1990 |
1991 | 1. Let |idx| be an {{unsigned long long}}, initially 0.
1992 |
1993 | 1. Let |accumulator| be |initialValue| if it is given, and uninitialized otherwise.
1994 |
1995 | 1. Let |observer| be a new [=internal observer=], initialized as follows:
1996 |
1997 | : [=internal observer/next steps=]
1998 | ::
1999 | 1. If |accumulator| is uninitialized (meaning no |initialValue| was passed in), then set
2000 | |accumulator| to the passed in |value|, set |idx| to |idx| + 1, and abort these steps.
2001 |
2002 | Note: This means that |reducer| will not be called with the first |value| that [=this=]
2003 | produces set as the {{Reducer/currentValue}}. Rather, when the *second* value is
2004 | eventually emitted, we will call |reducer| with *it* as the {{Reducer/currentValue}},
2005 | and the first value (that we're saving here) as the {{Reducer/accumulator}}.
2006 |
2007 | 1. [=Invoke=] |reducer| with «|accumulator| as {{Reducer/accumulator}}, the passed in
2008 | |value| as {{Reducer/currentValue}}, |idx| as {{Reducer/index}}» and "`rethrow`". Let
2009 | |result| be the returned value.
2010 |
2011 | If
an exception |E| was thrown, then
2012 | [=reject=] |p| with |E|, and [=AbortController/signal abort=] |controller| with |E|.
2013 |
2014 | 1. Set |idx| to |idx| + 1.
2015 |
2016 | 1. Set |accumulator| to |result|.
2017 |
2018 | : [=internal observer/error steps=]
2019 | :: [=Reject=] |p| with the passed in
error.
2020 |
2021 | : [=internal observer/complete steps=]
2022 | :: 1. If |accumulator| is not "
unset", then [=resolve=] |p| with |accumulator|.
2023 |
2024 | Otherwise, [=reject=] |p| with a {{TypeError}}.
2025 |
2026 | 1.
Subscribe to [=this=] given |observer|
2027 | and |internal options|.
2028 |
2029 | 1. Return |p|.
2030 |
2031 |
2032 |
2033 | {{EventTarget}} integration
2034 |
2035 |
2036 | dictionary ObservableEventListenerOptions {
2037 | boolean capture = false;
2038 | boolean passive;
2039 | };
2040 |
2041 | partial interface EventTarget {
2042 | Observable when(DOMString type, optional ObservableEventListenerOptions options = {});
2043 | };
2044 |
2045 |
2046 |
2047 | The when(|type|, |options|) method steps are:
2048 |
2049 | 1. If [=this=]'s [=relevant global object=] is a {{Window}} object, and its [=associated
2050 | Document=] is not [=Document/fully active=], then return.
2051 |
2052 | 1. Let |event target| be [=this=].
2053 |
2054 | 1. Let |observable| be a [=new=] {{Observable}}, initialized as follows:
2055 |
2056 | : [=Observable/subscribe callback=]
2057 | :: An algorithm that takes a {{Subscriber}} |subscriber| and runs these steps:
2058 |
2059 | 1. If |event target| is null, abort these steps.
2060 |
2061 | Note: This is meant to capture the fact that |event target| can be garbage collected
2062 | by the time this algorithm runs upon subscription.
2063 |
2064 | 1. If |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=]
2065 | is [=AbortSignal/aborted=], abort these steps.
2066 |
2067 | 1. [=Add an event listener=] with |event target| and an [=event listener=] defined as
2068 | follows:
2069 |
2070 | : [=event listener/type=]
2071 | :: |type|
2072 |
2073 | : [=event listener/callback=]
2074 | :: The result of creating a new Web IDL {{EventListener}} instance representing a
2075 | reference to a function of one argument of type {{Event}} |event|. This function
2076 | executes the [=observable event listener invoke algorithm=] given |subscriber| and
2077 | |event|.
2078 |
2079 | : [=event listener/capture=]
2080 | :: |options|'s {{ObservableEventListenerOptions/capture}}
2081 |
2082 | : [=event listener/passive=]
2083 | :: |options|'s {{ObservableEventListenerOptions/passive}} if this member
2084 | [=map/exists=]; null otherwise.
2085 |
2086 | : [=event listener/once=]
2087 | :: false
2088 |
2089 | : [=event listener/signal=]
2090 | :: |subscriber|'s [=Subscriber/subscription controller=]'s [=AbortController/signal=]
2091 |
2092 | Note: This ensures that the [=event listener=] is cleaned up when
2093 | [=Subscriber/subscription controller=]'s [=AbortController/signal=] is
2094 | [=AbortSignal/aborted=], regardless of an engine's ownership model.
2095 |
2096 | 1. Return |observable|.
2097 |
2098 |
2099 |
2100 | The observable event listener invoke algorithm takes a {{Subscriber}} |subscriber| and
2101 | an {{Event}} |event|, and runs these steps:
2102 |
2103 | 1. Run |subscriber|'s {{Subscriber/next()}} method with |event|.
2104 |
2105 |
2106 |
2107 | /dom/observable/tentative/observable-event-target.any.js
2108 | /dom/observable/tentative/observable-event-target.window.js
2109 |
2110 |
2111 |
2112 | Security & Privacy Considerations
2113 |
2114 | This material is being upstreamed from our explainer into this specification, and in the meantime
2115 | you can consult the following resources:
2116 |
2117 | * [TAG Security/Privacy
2118 | Questionnaire](https://github.com/WICG/observable/blob/master/security-privacy-questionnaire.md)
2119 |
2120 | Acknowledgements
2121 |
2122 | A special thanks to [Ben Lesh](https://benlesh.com/) for much of the design input for the
2123 | {{Observable}} API, and his many years of work maintaining userland Observable code that made this
2124 | contribution to the web platform possible.
2125 |
--------------------------------------------------------------------------------