{"id":16640399,"url":"https://github.com/loreanvictor/quel","last_synced_at":"2025-03-16T22:31:23.655Z","repository":{"id":64094828,"uuid":"573361402","full_name":"loreanvictor/quel","owner":"loreanvictor","description":"Reactive Expressions for JavaScript","archived":false,"fork":false,"pushed_at":"2024-06-07T17:59:30.000Z","size":1818,"stargazers_count":27,"open_issues_count":6,"forks_count":2,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-02-27T14:58:03.365Z","etag":null,"topics":["functional-programming","javascript","observable","reactive-programming","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/loreanvictor.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-12-02T09:29:14.000Z","updated_at":"2024-05-30T22:46:53.000Z","dependencies_parsed_at":"2024-06-07T19:07:27.102Z","dependency_job_id":"6d77ef4a-e47a-46a4-b9ef-e9d2b8236e23","html_url":"https://github.com/loreanvictor/quel","commit_stats":{"total_commits":69,"total_committers":1,"mean_commits":69.0,"dds":0.0,"last_synced_commit":"7e591ee6a2f62cacdc2a8b5f3e1e40a665db9042"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loreanvictor%2Fquel","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loreanvictor%2Fquel/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loreanvictor%2Fquel/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loreanvictor%2Fquel/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/loreanvictor","download_url":"https://codeload.github.com/loreanvictor/quel/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243830953,"owners_count":20354854,"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":["functional-programming","javascript","observable","reactive-programming","typescript"],"created_at":"2024-10-12T07:08:45.580Z","updated_at":"2025-03-16T22:31:23.328Z","avatar_url":"https://github.com/loreanvictor.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"right\"\u003e\n\n[![npm package minimized gzipped size)](https://img.shields.io/bundlejs/size/quel?style=flat-square\u0026label=%20\u0026color=black)](https://bundlejs.com/?q=quel)\n[![types](https://img.shields.io/npm/types/quel?label=\u0026color=black\u0026style=flat-square)](./src/types.ts)\n[![version](https://img.shields.io/npm/v/quel?label=\u0026color=black\u0026style=flat-square)](https://www.npmjs.com/package/quel)\n[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/loreanvictor/quel/coverage.yml?label=%20\u0026style=flat-square)](https://github.com/loreanvictor/quel/actions/workflows/coverage.yml)\n\n\u003c/div\u003e\n\n\u003cimg src=\"misc/dark.svg#gh-dark-mode-only\" height=\"96px\"/\u003e\n\u003cimg src=\"misc/light.svg#gh-light-mode-only\" height=\"96px\"/\u003e\n\n_Reactive Expressions for JavaScript_\n\n```bash\nnpm i quel\n```\n\n**quel** is a tiny library for reactive programming in JavaScript. Use it to write applications that handle user interactions, events, timers, web sockets, etc. using only simple functions.\n\n```js\nimport { from, observe } from 'quel'\n\n\nconst div$ = document.querySelector('div')\n\n// 👇 encapsulate the value of the input\nconst input = from(document.querySelector('textarea'))\n\n// 👇 compute some other values based on that (changing) value\nconst chars = $ =\u003e $(input)?.length ?? 0\nconst words = $ =\u003e $(input)?.split(' ').length ?? 0\n\n// 👇 use the calculated value in a side-effect\nobserve($ =\u003e div$.textContent = `${$(chars)} chars, ${$(words)} words`)\n```\n\n\u003cdiv align=\"right\"\u003e\n\n[**▷ TRY IT**](https://stackblitz.com/edit/js-jh6zt2?file=index.html,index.js)\n\n\u003c/div\u003e\n\n\u003cbr\u003e\n\n**quel** focuses on simplicity and composability. Even complex scenarios (such as higher-order reactive sources, debouncing events, etc.)\nare implemented with plain JS functions combined with each other (instead of operators, hooks, or other custom abstractions).\n\n```js\n//\n// this code creates a timer whose rate changes\n// based on the value of an input\n//\n\nimport { from, observe, Timer } from 'quel'\n\n\nconst div$ = document.querySelector('div')\nconst input = from(document.querySelector('input'))\nconst rate = $ =\u003e parseInt($(input) ?? 100)\n\n//\n// 👇 wait a little bit after the input value is changed (debounce),\n//    then create a new timer with the new rate.\n//\n//    `timer` is a \"higher-order\" source of change, because\n//    its rate also changes based on the value of the input.\n//\nconst timer = async $ =\u003e {\n  await sleep(200)\n  return $(rate) \u0026\u0026 new Timer($(rate))\n}\n\nobserve($ =\u003e {\n  //\n  // 👇 `$(timer)` would yield the latest timer, \n  //     and `$($(timer))` would yield the latest\n  //     value of that timer, which is what we want to display.\n  //\n  const elapsed = $($(timer)) ?? '-'\n  div$.textContent = `elapsed: ${elapsed}`\n})\n```\n\n\u003cdiv align=\"right\"\u003e\n\n[**▷ TRY IT**](https://stackblitz.com/edit/js-4wppcl?file=index.js)\n\n\u003c/div\u003e\n\n\u003cbr\u003e\n\n# Contents\n\n- [Installation](#installation)\n- [Usage](#usage)\n  - [Sources](#sources)\n  - [Expressions](#expressions)\n  - [Observation](#observation)\n  - [Iteration](#iteration)\n  - [Cleanup](#cleanup)\n  - [Typing](#typing)\n  - [Custom Sources](#custom-sources)\n- [Features](#features)\n- [Related Work](#related-work)\n- [Contribution](#contribution)\n\n\u003cbr\u003e\n\n# Installation\n\nOn [node](https://nodejs.org/en/):\n```bash\nnpm i quel\n```\nOn browser (or [deno](https://deno.land)):\n```js\nimport { from, observe } from 'https://esm.sh/quel'\n```\n\n\u003cbr\u003e\n\n# Usage\n\n1. Encapsulate (or create) [sources of change](#sources),\n  ```js\n  const timer = new Timer(1000)\n  const input = from(document.querySelector('#my-input'))\n  ```\n2. Process and combine these changing values using [simple functions](#expressions),\n  ```js\n  const chars = $ =\u003e $(input).length\n  ```\n3. [Observe](#observation) these changing values and react to them\n   (or [iterate](#iteration) over them),\n  ```js\n  const obs = observe($ =\u003e console.log($(timer) + ' : ' + $(chars)))\n  ```  \n4. [Clean up](#cleanup) the sources, releasing resources (e.g. stop a timer, remove an event listener, cloe a socket, etc.).\n  ```js\n  obs.stop()\n  timer.stop()\n  ```\n\n\u003cbr\u003e\n\n## Sources\n\n📝 Create a subject (whose value you can manually set at any time):\n```js\nimport { Subject } from 'quel'\n\nconst a = new Subject()\na.set(2)\n```\n🕑 Create a timer:\n```js\nimport { Timer } from 'quel'\n\nconst timer = new Timer(1000)\n```\n⌨️ Create an event source:\n```js\nimport { from } from 'quel'\n\nconst click = from(document.querySelector('button'))\nconst hover = from(document.querySelector('button'), 'hover')\nconst input = from(document.querySelector('input'))\n```\n👀 Read latest value of a source:\n```js\nsrc.get()\n```\n✋ Stop a source:\n```js\nsrc.stop()\n```\n💁‍♂️ Wait for a source to be stopped:\n```js\nawait src.stops()\n```\n\n\u003cbr\u003e\n\n\u003e In runtimes supporting `using` keyword ([see proposal](https://github.com/tc39/proposal-explicit-resource-management)), you can\n\u003e subscribe to a source:\n\u003e ```js\n\u003e using sub = src.subscribe(value =\u003e ...)\n\u003e ```\n\u003e Currently [TypeScript 5.2](https://devblogs.microsoft.com/typescript/announcing-typescript-5-2-beta/#using-declarations-and-explicit-resource-management) or later supports `using` keyword.\n\n\u003cbr\u003e\n\n## Expressions\n\n⛓️ Combine two sources using simple _expression_ functions:\n```js\nconst sum = $ =\u003e $(a) + $(b)\n```\n🔍 Filter values:\n```js\nimport { SKIP } from 'quel'\n\nconst odd = $ =\u003e $(a) % 2 === 0 ? SKIP : $(a)\n```\n🔃 Expressions can be async:\n```js\nconst response = async $ =\u003e {\n  // a debounce to avoid overwhelming the\n  // server with requests.\n  await sleep(200)\n  \n  if ($(query)) {\n    try {\n      const res = await fetch('https://pokeapi.co/api/v2/pokemon/' + $(query))\n      const json = await res.json()\n\n      return JSON.stringify(json, null, 2)\n    } catch {\n      return 'Could not find Pokemon'\n    }\n  }\n}\n```\n\n\u003cdiv align=\"right\"\u003e\n\n[**▷ TRY IT**](https://stackblitz.com/edit/js-3jpams?file=index.js)\n\n\u003c/div\u003e\n\n🫳 Flatten higher-order sources:\n```js\nconst variableTimer = $ =\u003e new Timer($(input))\nconst message = $ =\u003e 'elapsed: ' + $($(timer))\n```\n✋ Stop the expression:\n```js\nimport { STOP } from 'quel'\n\nlet count = 0\nconst take5 = $ =\u003e {\n  if (count++ \u003e 5) return STOP\n\n  return $(src)\n}\n```\n\n\u003cbr\u003e\n\n\u003e ℹ️ **IMPORTANT**\n\u003e\n\u003e The `$` function, passed to expressions, _tracks_ and returns the latest value of a given source. Expressions\n\u003e are then re-run every time a tracked source has a new value. Make sure you track the same sources everytime\n\u003e the expression runs.\n\u003e\n\u003e **DO NOT** create sources you want to track inside an expression:\n\u003e \n\u003e ```js\n\u003e // 👇 this is WRONG ❌\n\u003e const computed = $ =\u003e $(new Timer(1000)) * 2\n\u003e ```\n\u003e ```js\n\u003e // 👇 this is CORRECT ✅\n\u003e const timer = new Timer(1000)\n\u003e const computed = $ =\u003e $(timer) * 2\n\u003e ```\n\u003e\n\u003e \u003cbr/\u003e\n\u003e \n\u003e You _CAN_ create new sources inside an expression and return them (without tracking) them, creating a higher-order source:\n\u003e ```js\n\u003e //\n\u003e // this is OK ✅\n\u003e // `timer` is a source of changing timers, \n\u003e // who themselves are a source of changing numbers.\n\u003e //\n\u003e const timer = $ =\u003e new Timer($(rate))\n\u003e ```\n\u003e ```js\n\u003e //\n\u003e // this is OK ✅\n\u003e // `$(timer)` returns the latest timer as long as a new timer\n\u003e // is not created (in response to a change in `rate`), so this\n\u003e // expression is re-evaluated only when it needs to.\n\u003e //\n\u003e const msg = $ =\u003e 'elapsed: ' + $($(timer))\n\u003e ```\n\u003cbr\u003e\n\n## Observation\n\n🚀 Run side effects:\n```js\nimport { observe } from 'quel'\n\nobserve($ =\u003e console.log($(message)))\n```\n\n💡 Observations are sources themselves:\n```js\nconst y = observe($ =\u003e $(x) * 2)\nconsole.log(y.get())\n```\n\n✋ Don't forget to stop observations:\n\n```js\nconst obs = observe($ =\u003e ...)\nobs.stop()\n```\n\n\u003cbr\u003e\n\n\u003e In runtimes supporting `using` keyword ([see proposal](https://github.com/tc39/proposal-explicit-resource-management)), you don't need to manually stop observations:\n\u003e ```js\n\u003e using obs = observe($ =\u003e ...)\n\u003e ```\n\u003e Currently [TypeScript 5.2](https://devblogs.microsoft.com/typescript/announcing-typescript-5-2-beta/#using-declarations-and-explicit-resource-management) or later supports `using` keyword.\n\n\u003cbr\u003e\n\nAsync expressions might get aborted mid-execution. You can handle those events by passing a second argument to `observe()`:\n```js\nlet ctrl = new AbortController()\n\nconst data = observe(async $ =\u003e {\n  await sleep(200)\n  \n  // 👇 pass abort controller signal to fetch to cancel mid-flight requests\n  const res = await fetch('https://my.api/?q=' + $(input), {\n    signal: ctrl.signal\n  })\n\n  return await res.json()\n}, () =\u003e {\n  ctrl.abort()\n  ctrl = new AbortController()\n})\n```\n\n\u003cbr\u003e\n\n## Iteration\n\nIterate on values of a source using `iterate()`:\n```js\nimport { iterate } from 'quel'\n\nfor await (const i of iterate(src)) {\n  // do something with it\n}\n```\nIf the source emits values faster than you consume them, you are going to miss out on them:\n```js\nconst timer = new Timer(500)\n\n// 👇 loop body is slower than the source. values will be lost!\nfor await (const i of iterate(timer)) {\n  await sleep(1000)\n  console.log(i)\n}\n```\n\n\u003cdiv align=\"right\"\u003e\n\n[**▷ TRY IT**](https://codepen.io/lorean_victor/pen/abKxbNw?editors=1010)\n\n\u003c/div\u003e\n\n## Cleanup\n\n🧹 You need to manually clean up sources you create:\n\n```js\nconst timer = new Timer(1000)\n\n// ... whatever ...\n\ntimer.stop()\n```\n\n✨ Observations cleanup automatically when all their tracked sources\nstop. YOU DONT NEED TO CLEANUP OBSERVATIONS.\n\nIf you want to stop an observation earlier, call `stop()` on it:\n\n```js\nconst obs = observe($ =\u003e $(src))\n\n// ... whatever ...\n\nobs.stop()\n```\n\n\u003cbr\u003e\n\n\u003e In runtimes supporting `using` keyword ([see proposal](https://github.com/tc39/proposal-explicit-resource-management)), you can safely\n\u003e create sources without manually cleaning them up:\n\u003e ```js\n\u003e using timer = new Timer(1000)\n\u003e ```\n\u003e Currently [TypeScript 5.2](https://devblogs.microsoft.com/typescript/announcing-typescript-5-2-beta/#using-declarations-and-explicit-resource-management) or later supports `using` keyword.\n\n\n\u003cbr\u003e\n\n## Typing\n\nTypeScript wouldn't be able to infer proper types for expressions. To resolve this issue, use `Track` type:\n\n```ts\nimport { Track } from 'quel'\n\nconst expr = ($: Track) =\u003e $(a) * 2\n```\n\n👉 [Check this](src/types.ts) for more useful types.\n\n\u003cbr\u003e\n\n## Custom Sources\n\nCreate your own sources using `Source` class:\n\n```js\nconst src = new Source(async emit =\u003e {\n  await sleep(1000)\n  emit('Hellow World!')\n})\n```\n\nIf cleanup is needed, and your producer is sync, return a cleanup function:\n```js\nconst myTimer = new Source(emit =\u003e {\n  let i = 0\n  const interval = setInterval(() =\u003e emit(++i), 1000)\n  \n  // 👇 clear the interval when the source is stopped\n  return () =\u003e clearInterval(interval)\n})\n```\n\nIf your producer is async, register the cleanup using `finalize` callback:\n```js\n// 👇 with async producers, use a callback to specify cleanup code\nconst asyncTimer = new Source(async (emit, finalize) =\u003e {\n  let i = 0\n  let stopped = false\n  \n  finalize(() =\u003e stopped = true)\n  \n  while (!stopped) {\n    emit(++i)\n    await sleep(1000)\n  }\n})\n```\n\nYou can also extend the `Source` class:\n\n```js\nclass MyTimer extends Source {\n  constructor(rate = 200) {\n    super()\n    this.rate = rate\n    this.count = 0\n  }\n  \n  toggle() {\n    if (this.interval) {\n      this.interval = clearInterval(this.interval)\n    } else {\n      this.interval = setInterval(\n        // call this.emit() to emit values\n        () =\u003e this.emit(++this.count),\n        this.rate\n      )\n    }\n  }\n  \n  // override stop() to clean up\n  stop() {\n    clearInterval(this.interval)\n    super.stop()\n  }\n}\n```\n\n\u003cdiv align=\"right\"\u003e\n\n[**▷ TRY IT**](https://codepen.io/lorean_victor/pen/WNPdBdx?editors=0011)\n\n\u003c/div\u003e\n\n# Features\n\n🧩 [**quel**](.) has a minimal API surface (the whole package [is ~1.3KB](https://bundlephobia.com/package/quel@0.1.5)), and relies on composability instead of providng tons of operators / helper methods:\n\n```js\n// combine two sources:\n$ =\u003e $(a) + $(b)\n```\n```js\n// debounce:\nasync $ =\u003e {\n  await sleep(1000)\n  return $(src)\n}\n```\n```js\n// flatten (e.g. switchMap):\n$ =\u003e $($(src))\n```\n```js\n// filter a source\n$ =\u003e $(src) % 2 === 0 ? $(src) : SKIP\n```\n```js\n// take until other source emits a value\n$ =\u003e !$(notifier) ? $(src) : STOP\n```\n```js\n// batch emissions\nasync $ =\u003e (await Promise.resolve(), $(src))\n```\n```js\n// batch with animation frames\nasync $ =\u003e {\n  await Promise(resolve =\u003e requestAnimationFrame(resolve))\n  return $(src)\n}\n```\n```js\n// merge sources\nnew Source(emit =\u003e {\n  const obs = sources.map(src =\u003e observe($ =\u003e emit($(src))))\n  return () =\u003e obs.forEach(ob =\u003e ob.stop())\n})\n```\n```js\n// throttle\nlet timeout = null\n  \n$ =\u003e {\n  const value = $(src)\n  if (timeout === null) {\n    timeout = setTimeout(() =\u003e timeout = null, 1000)\n    return value\n  } else {\n    return SKIP\n  }\n}\n```\n\n\n\u003cbr\u003e\n\n🛂 [**quel**](.) is imperative (unlike most other general-purpose reactive programming libraries such as [RxJS](https://rxjs.dev), which are functional), resulting in code that is easier to read, write and debug:\n\n```js\nimport { interval, map, filter } from 'rxjs'\n\nconst a = interval(1000)\nconst b = interval(500)\n\ncombineLatest(a, b).pipe(\n  map(([x, y]) =\u003e x + y),\n  filter(x =\u003e x % 2 === 0),\n).subscribe(console.log)\n```\n```js\nimport { Timer, observe } from 'quel'\n\nconst a = new Timer(1000)\nconst b = new Timer(500)\n\nobserve($ =\u003e {\n  const sum = $(a) + $(b)\n  if (sum % 2 === 0) {\n    console.log(sum)\n  }\n})\n```\n\n\u003cbr\u003e\n\n⚡ [**quel**](.) is as fast as [RxJS](https://rxjs.dev). Note that in most cases performance is not the primary concern when programming reactive applications (since you are handling async events). If performance is critical for your use case, I'd recommend using likes of [xstream](http://staltz.github.io/xstream/) or [streamlets](https://github.com/loreanvictor/streamlet), as the imperative style of [**quel**](.) does tax a performance penalty inevitably compared to the fastest possible implementation.\n\n\u003cbr\u003e\n\n🧠 [**quel**](.) is more memory-intensive than [RxJS](https://rxjs.dev). Similar to the unavoidable performance tax, tracking sources of an expression will use more memory compared to explicitly tracking and specifying them.\n\n\u003cbr\u003e\n\n☕ [**quel**](.) only supports [hot](https://rxjs.dev/guide/glossary-and-semantics#hot) [listenables](https://rxjs.dev/guide/glossary-and-semantics#push). Certain use cases would benefit (for example, in terms of performance) from using cold listenables, or from having hybrid pull-push primitives. However,  most common event sources (user events, timers, Web Sockets, etc.) are hot listenables, and [**quel**](.) does indeed use the limited scope for simplification and optimization of its code.\n\n\u003cbr\u003e\n\n# Related Work\n\n- [**quel**](.) is inspired by [rxjs-autorun](https://github.com/kosich/rxjs-autorun) by [@kosich](https://github.com/kosich).\n- [**quel**](.) is basically an in-field experiment on ideas discussed in detail [here](https://github.com/loreanvictor/reactive-javascript).\n- [**quel**](.)'s focus on hot listenables was inspired by [xstream](https://github.com/staltz/xstream).\n\n\u003cbr\u003e\n\n# Contribution\n\nYou need [node](https://nodejs.org/en/), [NPM](https://www.npmjs.com) to start and [git](https://git-scm.com) to start.\n\n```bash\n# clone the code\ngit clone git@github.com:loreanvictor/quel.git\n```\n```bash\n# install stuff\nnpm i\n```\n\nMake sure all checks are successful on your PRs. This includes all tests passing, high code coverage, correct typings and abiding all [the linting rules](https://github.com/loreanvictor/quel/blob/main/.eslintrc). The code is typed with [TypeScript](https://www.typescriptlang.org), [Jest](https://jestjs.io) is used for testing and coverage reports, [ESLint](https://eslint.org) and [TypeScript ESLint](https://typescript-eslint.io) are used for linting. Subsequently, IDE integrations for TypeScript and ESLint would make your life much easier (for example, [VSCode](https://code.visualstudio.com) supports TypeScript out of the box and has [this nice ESLint plugin](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)), but you could also use the following commands:\n\n```bash\n# run tests\nnpm test\n```\n```bash\n# check code coverage\nnpm run coverage\n```\n```bash\n# run linter\nnpm run lint\n```\n```bash\n# run type checker\nnpm run typecheck\n```\n\nYou can also use the following commands to run performance benchmarks:\n\n```bash\n# run all benchmarks\nnpm run bench\n```\n```bash\n# run performance benchmarks\nnpm run bench:perf\n```\n```bash\n# run memory benchmarks\nnpm run bench:mem\n```\n\n\u003cbr\u003e\u003cbr\u003e\n\n\u003cdiv align=\"center\"\u003e\n\u003cimg src=\"chameleon.png\" width=\"256px\" /\u003e\n\u003c/div\u003e\n\n\u003cbr\u003e\u003cbr\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Floreanvictor%2Fquel","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Floreanvictor%2Fquel","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Floreanvictor%2Fquel/lists"}