{"id":15189742,"url":"https://github.com/WICG/observable","last_synced_at":"2025-10-02T04:31:46.405Z","repository":{"id":172119096,"uuid":"615770888","full_name":"WICG/observable","owner":"WICG","description":"Observable API proposal","archived":false,"fork":false,"pushed_at":"2024-12-12T03:54:37.000Z","size":527,"stargazers_count":600,"open_issues_count":38,"forks_count":16,"subscribers_count":31,"default_branch":"master","last_synced_at":"2024-12-12T04:26:23.631Z","etag":null,"topics":["dom","html","javascript","observable","observables","reactive-programming","web-application","whatwg"],"latest_commit_sha":null,"homepage":"https://wicg.github.io/observable/","language":"Bikeshed","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/WICG.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"security-privacy-questionnaire.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-03-18T16:24:03.000Z","updated_at":"2024-12-12T03:53:38.000Z","dependencies_parsed_at":"2023-11-10T21:24:39.855Z","dependency_job_id":"07b1a677-9f6b-4b73-9025-78a50a5b79ed","html_url":"https://github.com/WICG/observable","commit_stats":null,"previous_names":["domfarolino/observable","wicg/observable"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WICG%2Fobservable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WICG%2Fobservable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WICG%2Fobservable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WICG%2Fobservable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/WICG","download_url":"https://codeload.github.com/WICG/observable/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":234934648,"owners_count":18909704,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dom","html","javascript","observable","observables","reactive-programming","web-application","whatwg"],"created_at":"2024-09-27T20:02:05.578Z","updated_at":"2025-10-02T04:31:46.391Z","avatar_url":"https://github.com/WICG.png","language":"Bikeshed","funding_links":[],"categories":["Bikeshed","The Observable Standard"],"sub_categories":["Podcasts"],"readme":"# Observable\n\nThis is the explainer for the Observable API proposal for more ergonomic and\ncomposable event handling.\n\n## Introduction\n\n### `EventTarget` integration\n\nThis proposal adds a `.when()` method to `EventTarget` that becomes a better\n`addEventListener()`; specifically it returns a [new\n`Observable`](#the-observable-api) that adds a new event listener to the target\nwhen its `subscribe()` method is called. The Observable calls the subscriber's\n`next()` handler with each event.\n\nObservables turn event handling, filtering, and termination, into an explicit, declarative flow\nthat's easier to understand and\n[compose](https://stackoverflow.com/questions/44112364/what-does-this-mean-in-the-observable-tc-39-proposal)\nthan today's imperative version, which often requires nested calls to `addEventListener()` and\nhard-to-follow callback chains.\n\n#### Example 1\n\n```js\n// Filtering and mapping:\nelement\n\t.when('click')\n\t.filter((e) =\u003e e.target.matches('.foo'))\n\t.map((e) =\u003e ({ x: e.clientX, y: e.clientY }))\n\t.subscribe({ next: handleClickAtPoint });\n```\n\n#### Example 2\n\n```js\n// Automatic, declarative unsubscription via the takeUntil method:\nelement.when('mousemove')\n  .takeUntil(document.when('mouseup'))\n  .subscribe({next: e =\u003e … });\n\n// Since reduce and some other terminators return promises, they also play\n// well with async functions:\nawait element.when('mousemove')\n  .takeUntil(element.when('mouseup'))\n  .reduce((soFar, e) =\u003e …);\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eImperative version\u003c/summary\u003e\n\n```js\n// Imperative\nconst controller = new AbortController();\nelement.addEventListener('mousemove', e =\u003e {\n  console.log(e);\n\n  element.addEventListener('mouseup', e =\u003e {\n    controller.abort();\n  });\n}, { signal: controller.signal });\n```\n\n\u003c/details\u003e\n\n#### Example 3\n\nTracking all link clicks within a container\n([example](https://github.com/whatwg/dom/issues/544#issuecomment-351705380)):\n\n```js\ncontainer\n\t.when('click')\n\t.filter((e) =\u003e e.target.closest('a'))\n\t.subscribe({\n\t\tnext: (e) =\u003e {\n\t\t\t// …\n\t\t},\n\t});\n```\n\n#### Example 4\n\nFind the maximum Y coordinate while the mouse is held down\n([example](https://github.com/whatwg/dom/issues/544#issuecomment-351762493)):\n\n```js\nconst maxY = await element\n\t.when('mousemove')\n\t.takeUntil(element.when('mouseup'))\n\t.map((e) =\u003e e.clientY)\n\t.reduce((soFar, y) =\u003e Math.max(soFar, y), 0);\n```\n\n#### Example 5\n\nMultiplexing a `WebSocket`, such that a subscription message is send on connection,\nand an unsubscription message is send to the server when the user unsubscribes.\n\n```js\nconst socket = new WebSocket('wss://example.com');\n\nfunction multiplex({ startMsg, stopMsg, match }) {\n  if (socket.readyState !== WebSocket.OPEN) {\n    return socket\n      .when('open')\n      .flatMap(() =\u003e multiplex({ startMsg, stopMsg, match }));\n  } else {\n    socket.send(JSON.stringify(startMsg));\n    return socket\n      .when('message')\n      .filter(match)\n      .takeUntil(socket.when('close'))\n      .takeUntil(socket.when('error'))\n      .map((e) =\u003e JSON.parse(e.data))\n      .finally(() =\u003e {\n        socket.send(JSON.stringify(stopMsg));\n      });\n  }\n}\n\nfunction streamStock(ticker) {\n  return multiplex({\n    startMsg: { ticker, type: 'sub' },\n    stopMsg: { ticker, type: 'unsub' },\n    match: (data) =\u003e data.ticker === ticker,\n  });\n}\n\nconst googTrades = streamStock('GOOG');\nconst nflxTrades = streamStock('NFLX');\n\nconst googController = new AbortController();\ngoogTrades.subscribe({next: updateView}, {signal: googController.signal});\nnflxTrades.subscribe({next: updateView, ...});\n\n// And the stream can disconnect later, which\n// automatically sends the unsubscription message\n// to the server.\ngoogController.abort();\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eImperative version\u003c/summary\u003e\n\n```js\n// Imperative\nfunction multiplex({ startMsg, stopMsg, match }) {\n\tconst start = (callback) =\u003e {\n\t\tconst teardowns = [];\n\n\t\tif (socket.readyState !== WebSocket.OPEN) {\n\t\t\tconst openHandler = () =\u003e start({ startMsg, stopMsg, match })(callback);\n\t\t\tsocket.addEventListener('open', openHandler);\n\t\t\tteardowns.push(() =\u003e {\n\t\t\t\tsocket.removeEventListener('open', openHandler);\n\t\t\t});\n\t\t} else {\n\t\t\tsocket.send(JSON.stringify(startMsg));\n\t\t\tconst messageHandler = (e) =\u003e {\n\t\t\t\tconst data = JSON.parse(e.data);\n\t\t\t\tif (match(data)) {\n\t\t\t\t\tcallback(data);\n\t\t\t\t}\n\t\t\t};\n\t\t\tsocket.addEventListener('message', messageHandler);\n\t\t\tteardowns.push(() =\u003e {\n\t\t\t\tsocket.send(JSON.stringify(stopMsg));\n\t\t\t\tsocket.removeEventListener('message', messageHandler);\n\t\t\t});\n\t\t}\n\n\t\tconst finalize = () =\u003e {\n\t\t\tteardowns.forEach((t) =\u003e t());\n\t\t};\n\n\t\tsocket.addEventListener('close', finalize);\n\t\tteardowns.push(() =\u003e socket.removeEventListener('close', finalize));\n\t\tsocket.addEventListener('error', finalize);\n\t\tteardowns.push(() =\u003e socket.removeEventListener('error', finalize));\n\n\t\treturn finalize;\n\t};\n\n\treturn start;\n}\n\nfunction streamStock(ticker) {\n\treturn multiplex({\n\t\tstartMsg: { ticker, type: 'sub' },\n\t\tstopMsg: { ticker, type: 'unsub' },\n\t\tmatch: (data) =\u003e data.ticker === ticker,\n\t});\n}\n\nconst googTrades = streamStock('GOOG');\nconst nflxTrades = streamStock('NFLX');\n\nconst unsubGoogTrades = googTrades(updateView);\nconst unsubNflxTrades = nflxTrades(updateView);\n\n// And the stream can disconnect later, which\n// automatically sends the unsubscription message\n// to the server.\nunsubGoogTrades();\n```\n\n\u003c/details\u003e\n\n#### Example 6\n\nHere we're leveraging observables to match a secret code, which is a pattern of\nkeys the user might hit while using an app:\n\n```js\nconst pattern = [\n  'ArrowUp',\n  'ArrowUp',\n  'ArrowDown',\n  'ArrowDown',\n  'ArrowLeft',\n  'ArrowRight',\n  'ArrowLeft',\n  'ArrowRight',\n  'b',\n  'a',\n  'Enter',\n];\n\nconst keys = document.when('keydown').map(e =\u003e e.key);\n\nkeys\n  .flatMap(firstKey =\u003e {\n    if (firstKey === pattern[0]) {\n      return keys\n        .take(pattern.length - 1)\n        .every((k, i) =\u003e k === pattern[i + 1]);\n    }\n  })\n  .filter(matched =\u003e matched)\n  .subscribe(() =\u003e console.log('Secret code matched!'));\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eImperative version\u003c/summary\u003e\n\n```js\nconst pattern = [...];\n\n// Imperative\ndocument.addEventListener('keydown', e =\u003e {\n  const key = e.key;\n  if (key === pattern[0]) {\n    let i = 1;\n    const handler = (e) =\u003e {\n      const nextKey = e.key;\n      if (nextKey !== pattern[i++]) {\n        document.removeEventListener('keydown', handler)\n      } else if (pattern.length === i) {\n        console.log('Secret code matched!');\n        document.removeEventListener('keydown', handler)\n      }\n    };\n\n    document.addEventListener('keydown', handler);\n  }\n}, {once: true});\n```\n\n\u003c/details\u003e\n\n### The `Observable` API\n\nObservables are first-class objects representing composable, repeated events.\nThey're like Promises but for multiple events, and specifically with\n[`EventTarget` integration](#eventtarget-integration), they are to events what\nPromises are to callbacks. They can be:\n\n- Created by script or by platform APIs, and passed to anyone interested in\n  consuming events via `subscribe()`\n- Fed to [operators](#operators) like `Observable.map()`, to be composed \u0026\n  transformed without a web of nested callbacks\n\nBetter yet, the transition from event handlers ➡️ Observables is simpler than\nthat of callbacks ➡️ Promises, since Observables integrate nicely on top of\n`EventTarget`, the de facto way of subscribing to events from the platform [and\ncustom script](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/EventTarget#examples).\nAs a result, developers can use Observables without migrating tons of code on\nthe platform, since it's an easy drop-in wherever you're handling events today.\n\nThe proposed API shape can be found in\nhttps://wicg.github.io/observable/#core-infrastructure.\n\nThe creator of an Observable passes in a callback that gets invoked\nsynchronously whenever `subscribe()` is called. The `subscribe()` method can be\ncalled _any number of times_, and the callback it invokes sets up a new\n\"subscription\" by registering the caller of `subscribe()` as a Observer. With\nthis in place, the Observable can signal any number of events to the Observer\nvia the `next()` callback, optionally followed by a single call to either\n`complete()` or `error()`, signaling that the stream of data is finished.\n\n```js\nconst observable = new Observable((subscriber) =\u003e {\n\tlet i = 0;\n\tsetInterval(() =\u003e {\n\t\tif (i \u003e= 10) subscriber.complete();\n\t\telse subscriber.next(i++);\n\t}, 2000);\n});\n\nobservable.subscribe({\n\t// Print each value the Observable produces.\n\tnext: console.log,\n});\n```\n\nWhile custom Observables can be useful on their own, the primary use case they\nunlock is with event handling. Observables returned by the new\n`EventTarget#when()` method are created natively with an internal callback that\nuses the same [underlying\nmechanism](https://dom.spec.whatwg.org/#add-an-event-listener) as\n`addEventListener()`. Therefore calling `subscribe()` essentially registers a\nnew event listener whose events are exposed through the Observer handler\nfunctions and are composable with the various\n[combinators](#operators) available to all Observables.\n\n#### Constructing \u0026 converting objects to Observables\n\nObservables can be created by their native constructor, as demonstrated above,\nor by the `Observable.from()` static method. This method constructs a native\nObservable from objects that are any of the following, _in this order_:\n\n- `Observable` (in which case it just returns the given object)\n- `AsyncIterable` (anything with `Symbol.asyncIterator`)\n- `Iterable` (anything with `Symbol.iterator`)\n- `Promise` (or any thenable)\n\nFurthermore, any method on the platform that wishes to accept an Observable as a\nWeb IDL argument, or return one from a callback whose return type is\n`Observable` can do so with any of the above objects as well, that get\nautomatically converted to an Observable. We can accomplish this in one of two\nways that we'll finalize in the Observable specification:\n\n1.  By making the `Observable` type a special Web IDL type that performs this\n    ECMAScript Object ➡️ Web IDL conversion automatically, like Web IDL does for\n    other types.\n2.  Require methods and callbacks that work with Observables to specify the type\n    `any`, and have the corresponding spec prose immediately invoke a conversion\n    algorithm that the Observable specification will supply. This is similar to\n    what the Streams Standard [does with async iterables\n    today](https://streams.spec.whatwg.org/#rs-from).\n\nThe conversation in https://github.com/domfarolino/observable/pull/60 leans\ntowards option (1).\n\n#### Lazy, synchronous delivery\n\nCrucially, Observables are \"lazy\" in that they do not start emitting data until\nthey are subscribed to, nor do they queue any data _before_ subscription. They\ncan also start emitting data synchronously during subscription, unlike Promises\nwhich always queue microtasks when invoking `.then()` handlers. Consider this\n[example](https://github.com/whatwg/dom/issues/544#issuecomment-351758385):\n\n```js\nel.when('click').subscribe({next: () =\u003e console.log('One')});\nel.when('click').find(() =\u003e {…}).then(() =\u003e console.log('Three'));\nel.click();\nconsole.log('Two');\n// Logs \"One\" \"Two\" \"Three\"\n```\n\n#### Firehose of synchronous data\n\nBy using `AbortController`, you can unsubscribe from an Observable even as it\nsynchronously emits data _during_ subscription:\n\n```js\n// An observable that synchronously emits unlimited data during subscription.\nlet observable = new Observable((subscriber) =\u003e {\n\tlet i = 0;\n\twhile (true) {\n\t\tsubscriber.next(i++);\n\t}\n});\n\nlet controller = new AbortController();\nobservable.subscribe({\n\tnext: (data) =\u003e {\n\t\tif (data \u003e 100) controller.abort();\n\t}}, {signal: controller.signal},\n});\n```\n\n#### Teardown\n\nIt is critical for an Observable subscriber to be able to register an arbitrary\nteardown callback to clean up any resources relevant to the subscription. The\nteardown can be registered from within the subscription callback passed into the\n`Observable` constructor. When run (upon subscribing), the subscription callback\ncan register a teardown function via `subscriber.addTeardown()`.\n\nIf the subscriber has already been aborted (i.e., `subscriber.signal.aborted` is\n`true`), then the given teardown callback is invoked immediately from within\n`addTeardown()`. Otherwise, it is invoked synchronously:\n\n- From `complete()`, after the subscriber's complete handler (if any) is\n  invoked\n- From `error()`, after the subscriber's error handler (if any) is invoked\n- The signal passed to the subscription is aborted by the user.\n\n### Operators\n\nWe propose the following operators in addition to the `Observable` interface:\n\n- `catch()`\n  - Like `Promise#catch()`, it takes a callback which gets fired after the source\n    observable errors. It will then map to a new observable, returned by the callback,\n    unless the error is rethrown.\n- `takeUntil(Observable)`\n  - Returns an observable that mirrors the one that this method is called on,\n    until the input observable emits its first value\n- `finally()`\n  - Like `Promise.finally()`, it takes a callback which gets fired after the\n    observable completes in any way (`complete()`/`error()`).\n  - Returns an `Observable` that mirrors the source observable exactly. The callback\n    passed to `finally` is fired when a subscription to the resulting observable is terminated\n    for _any reason_. Either immediately after the source completes or errors, or when the consumer\n    unsubscribes by aborting the subscription.\n\nVersions of the above are often present in userland implementations of\nobservables as they are useful for observable-specific reasons, but in addition\nto these we offer a set of common operators that follow existing platform\nprecedent and can greatly increase utility and adoption. These exist on other\niterables, and are derived from TC39's [iterator helpers\nproposal](https://github.com/tc39/proposal-iterator-helpers) which adds the\n[following\nmethods](https://tc39.es/proposal-iterator-helpers/#sec-iteratorprototype) to\n`Iterator.prototype`:\n\n- `map()`\n- `filter()`\n- `take()`\n- `drop()`\n- `flatMap()`\n- `reduce()`\n- `toArray()`\n- `forEach()`\n- `some()`\n- `every()`\n- `find()`\n\nAnd the following method statically on the `Iterator` constructor:\n\n- `from()`\n\nWe expect userland libraries to provide more niche operators that integrate with\nthe `Observable` API central to this proposal, potentially shipping natively if\nthey get enough momentum to graduate to the platform. But for this initial\nproposal, we'd like to restrict the set of operators to those that follow the\nprecedent stated above, similar to how web platform APIs that are declared\n[Setlike](https://webidl.spec.whatwg.org/#es-setlike) and\n[Maplike](https://webidl.spec.whatwg.org/#es-maplike) have native properties\ninspired by TC39's\n[Map](https://tc39.es/ecma262/#sec-properties-of-the-map-prototype-object) and\n[Set](https://tc39.es/ecma262/#sec-properties-of-the-set-prototype-object)\nobjects. Therefore we'd consider most discussion of expanding this set as\nout-of-scope for the _initial_ proposal, suitable for discussion in an appendix.\nAny long tail of operators could _conceivably_ follow along if there is support\nfor the native Observable API presented in this explainer.\n\nNote that the operators `every()`, `find()`, `some()`, and `reduce()` return\nPromises whose scheduling differs from that of Observables, which sometimes\nmeans event handlers that call `e.preventDefault()` will run too late. See the\n[Concerns](#concerns) section which goes into more detail.\n\n## Background \u0026 landscape\n\nTo illustrate how Observables fit into the current landscape of other reactive\nprimitives, see the below table which is an attempt at combining\n[two](https://github.com/kriskowal/gtor#a-general-theory-of-reactivity)\nother [tables](https://rxjs.dev/guide/observable) that classify reactive\nprimitives by their interaction with producers \u0026 consumers:\n\n\u003ctable\u003e\n  \u003cthead\u003e\n    \u003ctr\u003e\n      \u003cth\u003e\u003c/th\u003e\n      \u003cth colspan=\"2\"\u003eSingular\u003c/th\u003e\n      \u003cth colspan=\"2\"\u003ePlural\u003c/th\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003cth\u003e\u003c/td\u003e\n      \u003cth\u003eSpatial\u003c/th\u003e\n      \u003cth\u003eTemporal\u003c/th\u003e\n      \u003cth\u003eSpatial\u003c/th\u003e\n      \u003cth\u003eTemporal\u003c/th\u003e\n    \u003c/tr\u003e\n  \u003c/thead\u003e\n \u003ctbody\u003e\n    \u003ctr\u003e\n      \u003cth\u003ePush\u003c/th\u003e\n      \u003ctd\u003eValue\u003c/td\u003e\n      \u003ctd\u003ePromise\u003c/td\u003e\n      \u003ctd colspan=\"2\"\u003eObservable\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003cth\u003ePull\u003c/th\u003e\n      \u003ctd\u003eFunction\u003c/td\u003e\n      \u003ctd\u003eAsync iterator\u003c/td\u003e\n      \u003ctd\u003eIterable\u003c/td\u003e\n      \u003ctd\u003eAsync iterator\u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n### History\n\nObservables were first proposed to the platform in [TC39](https://github.com/tc39/proposal-observable)\nin May of 2015. The proposal failed to gain traction, in part due to some opposition that\nthe API was suitable to be a language-level primitive. In an attempt to renew the proposal\nat a higher level of abstraction, a WHATWG [DOM issue](https://github.com/whatwg/dom/issues/544) was\nfiled in December of 2017. Despite ample [developer demand](https://foolip.github.io/spec-reactions/),\n_lots_ of discussion, and no strong objectors, the DOM Observables proposal sat mostly still for several\nyears (with some flux in the API design) due to a lack of implementer prioritization.\n\nLater in 2019, [an attempt](https://github.com/tc39/proposal-observable/issues/201) at reviving the\nproposal was made back at the original TC39 repository, which involved some API simplifications and\nadded support for the synchronous \"firehose\" problem.\n\nThis repository is an attempt to again breathe life into the Observable proposal with the hope\nof shipping a version of it to the Web Platform.\n\n### Userland libraries\n\nIn [prior discussion](https://github.com/whatwg/dom/issues/544#issuecomment-1433955626),\n[Ben Lesh](https://github.com/benlesh) has listed several custom userland implementations of\nobservable primitives, of which RxJS is the most popular with \"47,000,000+ downloads _per week_.\"\n\n- [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.\n- [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.\n- [tRPC](https://github.com/trpc/trpc/blob/21bcb5e6723023d3acb0b836b63627922407c682/packages/server/src/observable/observable.ts): A nearly identical implemention of observable to this proposal.\n- [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.\n- [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.\n- [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.\n- [zen-observable](https://github.com/zenparsing/zen-observable/tree/8406a7e3a3a3faa080ec228b9a743f48021fba8b): A reference implementation of the TC39 observable proposal. Nearly identical to this proposal.\n- [React Router](https://github.com/remix-run/react-router/tree/610ce6edf0993384300ff3172fc6db00ead50d33): Uses a `{ subscribe(callback: (value: T) =\u003e void): () =\u003e 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.\n- [Preact](https://github.com/preactjs/preact/blob/ac1f145877a74e49f4c341e6acbf888a96e60afe/src/jsx.d.ts#LL69C1-L73C3) Uses a `{ subscribe(callback: (value: T) =\u003e void): () =\u003e void }` interface for their signals.\n- [TanStack](https://github.com/TanStack/query/blob/878d85e44c984822e2e868af94003ec260ddf80f/packages/query-core/src/subscribable.ts): Uses a subscribable interface that matches `{ subscribe(callback: (value: T) =\u003e void): () =\u003e void }` in [several places](https://github.com/search?q=repo%3ATanStack/query%20Subscribable\u0026type=code)\n- [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.\n- [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) =\u003e void): () =\u003e void }`.\n- [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.\n- [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)): () =\u003e void }`.\n\n### UI Frameworks Supporting Observables\n\n- [Svelte](https://github.com/sveltejs/svelte): Directly supports implicit subscription and unsubscription to observables simply by binding to them in templates.\n- [Angular](https://github.com/angular/angular): Directly supports implicit subscription and unsubscription to observables using their `| async` \"async pipe\" functionality in templates.\n- [Vue](https://github.com/vuejs/vuejs): maintains a [dedicated library](https://github.com/vuejs/vue-rx) specifically for using Vue with RxJS observables.\n- [Cycle.js](https://github.com/cyclejs/cyclejs): A UI framework built entirely around observables\n\nGiven the extensive prior art in this area, there exists a public\n\"[Observable Contract](https://reactivex.io/documentation/contract.html)\".\n\nAdditionally many JavaScript APIs been trying to adhere to the contract defined by the [TC39 proposal from 2015](https://github.com/tc39/proposal-observable).\nTo that end, there is a library, [symbol-observable](https://www.npmjs.com/package/symbol-observable?activeTab=dependents),\nthat ponyfills (polyfills) `Symbol.observable` to help with interoperability between observable types that adheres to exactly\nthe interface defined here. `symbol-observable` has 479 dependent packages on npm, and is downloaded more than 13,000,000 times\nper week. This means that there are a minimum of 479 packages on npm that are using the observable contract in some way.\n\nThis is similar to how [Promises/A+](https://promisesaplus.com/) specification that was developed before `Promise`s were\nadopted into ES2015 as a first-class language primitive.\n\n## Concerns\n\nOne of the main [concerns](https://github.com/whatwg/dom/issues/544#issuecomment-351443624)\nexpressed in the original WHATWG DOM thread has to do with Promise-ifying APIs on Observable,\nsuch as the proposed `first()`. The potential footgun here with microtask scheduling and event\nintegration. Specifically, the following innocent-looking code would not _always_ work:\n\n```js\nelement\n\t.when('click')\n\t.first()\n\t.then((e) =\u003e {\n\t\te.preventDefault();\n\t\t// Do something custom...\n\t});\n```\n\nIf `Observable#first()` returns a Promise that resolves when the first event is fired on an\n`EventTarget`, then the user-supplied Promise `.then()` handler will run:\n\n- ✅ Synchronously after event firing, for events triggered by the user\n- ❌ Asynchronously after event firing, for all events triggered by script (i.e., `element.click()`)\n  - This means `e.preventDefault()` will have happened too late and effectively been ignored\n\n\u003cdetails\u003e\nTo understand why this is the case, you must understand how and when the microtask queue is flushed\n(and thus how microtasks, including Promise resolution handlers, are invoked).\n\nIn WebIDL after a callback is invoked, the HTML algorithm\n_[clean up after running script](https://html.spec.whatwg.org/C#clean-up-after-running-script)_\n[is called](https://webidl.spec.whatwg.org/#ref-for-clean-up-after-running-script%E2%91%A0), and\nthis algorithm calls _[perform a microtask checkpoint](https://html.spec.whatwg.org/C#perform-a-microtask-checkpoint)_\nif and only if the JavaScript stack is empty.\n\nConcretely, that means for `element.click()` in the above example, the following steps occur:\n\n1. To run `element.click()`, a JavaScript execution context is first pushed onto the stack\n1. To run the internal `click` event listener callback (the one created natively by the\n   `Observable#from()` implementation), _another_ JavaScript execution context is pushed onto\n   the stack, as WebIDL prepares to run the internal callback\n1. The internal callback runs, which immediately resolves the Promise returned by `Observable#first()`;\n   now the microtask queue contains the Promise's user-supplied `then()` handler which will cancel\n   the event once it runs\n1. The top-most execution context is removed from the stack, and the microtask queue **cannot be\n   flushed**, because there is still JavaScript on the stack.\n1. After the internal `click` event callback is executed, the rest of the event path continues since\n   event was not canceled during or immediately after the callback. The event does whatever it would\n   normally do (submit the form, `alert()` the user, etc.)\n1. Finally, the JavaScript containing `element.click()` is finished, and the final execution context\nis popped from the stack and the microtask queue is flushed. The user-supplied `.then()` handler\nis run, which attempts to cancel the event too late\n\u003c/details\u003e\n\nTwo things mitigate this concern. First, there is a very simple workaround to _always_ avoid the\ncase where your `e.preventDefault()` might run too late:\n\n```js\nelement\n\t.when('click')\n\t.map((e) =\u003e (e.preventDefault(), e))\n\t.first();\n```\n\n...or if Observable had a `.do()` method (see https://github.com/whatwg/dom/issues/544#issuecomment-351457179):\n\n```js\nelement\n\t.when('click')\n\t.do((e) =\u003e e.preventDefault())\n\t.first();\n```\n\n...or by [modifying](https://github.com/whatwg/dom/issues/544#issuecomment-351779661) the semantics of\n`first()` to take a callback that produces a value that the returned Promise resolves to:\n\n```js\nel.when('submit')\n\t.first((e) =\u003e e.preventDefault())\n\t.then(doMoreStuff);\n```\n\nSecond, this \"quirk\" already exists in today's thriving Observable ecosystem, and there are no serious\nconcerns or reports from that community that developers are consistently running into this. This gives\nsome confidence that baking this behavior into the web platform will not be dangerous.\n\n## Standards venue\n\nThere's been much discussion about which standards venue should ultimately host an Observables\nproposal. The venue is not inconsequential, as it effectively decides whether Observables becomes a\nlanguage-level primitive like `Promise`s, that ship in all JavaScript browser engines, or a web platform\nprimitive with likely (but technically _optional_) consideration in other environments like Node.js\n(see [`AbortController`](https://nodejs.org/api/globals.html#class-abortcontroller) for example).\n\nObservables purposefully integrate frictionlessly with the main event-emitting interface\n(`EventTarget`) and cancellation primitive (`AbortController`) that live in the Web platform. As\nproposed here, observables join this existing strongly-connected component from the [DOM\nStandard](https://github.com/whatwg/dom): Observables depend on AbortController/AbortSignal, which\ndepend on EventTarget, and EventTarget depends on both Observables and AbortController/AbortSignal.\nBecause we feel that Observables fits in best where its supporting primitives live, the WHATWG\nstandards venue is probably the best place to advance this proposal. Additionally, non-Web\nECMAScript embedders like Node.js and Deno would still be able to adopt Observables, and are even\nlikely to, given their commitment to Web platform [aborting and\nevents](https://github.com/whatwg/dom/blob/bf5f6c2a8f2d770da884cb52f5625c59b5a880e7/PULL_REQUEST_TEMPLATE.md).\n\nThis does not preclude future standardization of event-emitting and cancellation primitives in TC39\nin the future, something Observables could theoretically be layered on top of later. But for now, we\nare motivated to make progress in WHATWG.\n\nIn attempt to avoid relitigating this discussion, we'd urge the reader to see the following\ndiscussion comments:\n\n- https://github.com/whatwg/dom/issues/544#issuecomment-351520728\n- https://github.com/whatwg/dom/issues/544#issuecomment-351561091\n- https://github.com/whatwg/dom/issues/544#issuecomment-351582862\n- https://github.com/whatwg/dom/issues/544#issuecomment-351607779\n- https://github.com/whatwg/dom/issues/544#issuecomment-351718686\n\n## Standards issues\n\nThis section bares a collection of web standards and standards positions issues\nused to track the Observable proposal's life outside of this repository.\n\n - [Mozilla standards\n   position](https://github.com/mozilla/standards-positions/issues/945)\n - [WebKit standards\n   position](https://github.com/WebKit/standards-positions/issues/292)\n - [Chrome Status](https://chromestatus.com/feature/5154593776599040)\n - [WinterCG](https://github.com/wintercg/proposal-common-minimum-api/issues/72)\n - [Node.js](https://github.com/nodejs/standards-positions/issues/1)\n - [W3C TAG review](https://github.com/w3ctag/design-reviews/issues/902)\n\n## User needs\n\nObservables are designed to make event handling more ergonomic and composable.\nAs such, their impact on end users is indirect, largely coming in the form of\nusers having to download less JavaScript to implement patterns that developers\ncurrently use third-party libraries for. As stated [above in the\nexplainer](https://github.com/domfarolino/observable#userland-libraries), there\nis a thriving userland Observables ecosystem which results in loads of excessive\nbytes being downloaded every day.\n\nIn an attempt to codify the strong userland precedent of the Observable API,\nthis proposal would save dozens of custom implementations from being downloaded\nevery day.\n\nAdditionally, as an API like `EventTarget`, `AbortController`, and one related\nto `Promise`s, it enables developers to build less-complicated event handling\nflows by constructing them declaratively, which may enable them to build more\nsound user experiences on the Web.\n\n## Authors:\n\n- [Dominic Farolino](https://github.com/domfarolino)\n- [Ben Lesh](https://github.com/benlesh)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FWICG%2Fobservable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FWICG%2Fobservable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FWICG%2Fobservable/lists"}