├── .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 | 488 | 489 | 490 | 491 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 |
SingularPlural
492 | SpatialTemporalSpatialTemporal
PushValuePromiseObservable
PullFunctionAsync iteratorIterableAsync iterator
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 | 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<sequence<any>> toArray(optional SubscribeOptions options = {}); 406 | Promise<undefined> forEach(Visitor callback, optional SubscribeOptions options = {}); 407 | Promise<boolean> every(Predicate predicate, optional SubscribeOptions options = {}); 408 | Promise<any> first(optional SubscribeOptions options = {}); 409 | Promise<any> last(optional SubscribeOptions options = {}); 410 | Promise<any> find(Predicate predicate, optional SubscribeOptions options = {}); 411 | Promise<boolean> some(Predicate predicate, optional SubscribeOptions options = {}); 412 | Promise<any> 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 |
  1. 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 |
  2. 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 | --------------------------------------------------------------------------------