{"id":15501720,"url":"https://github.com/deanrad/polyrhythm","last_synced_at":"2025-04-22T22:17:47.811Z","repository":{"id":44050022,"uuid":"218822404","full_name":"deanrad/polyrhythm","owner":"deanrad","description":"A 3Kb full-stack async effect management toolkit over RxJS. Uses a Pub-Sub paradigm to orchestrate Observables in Node, or the browser (ala Redux Saga). Exports: channel, listen, filter,  trigger, after.","archived":false,"fork":false,"pushed_at":"2023-01-07T11:17:12.000Z","size":2619,"stargazers_count":25,"open_issues_count":37,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-04-14T13:10:25.366Z","etag":null,"topics":["actor-model","async","command-object","javascript","observable","polyrhythm","redux","rxjs","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/deanrad.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-10-31T17:30:22.000Z","updated_at":"2022-09-11T17:04:52.000Z","dependencies_parsed_at":"2023-02-07T00:46:07.569Z","dependency_job_id":null,"html_url":"https://github.com/deanrad/polyrhythm","commit_stats":null,"previous_names":["deanius/polyrhythm"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deanrad%2Fpolyrhythm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deanrad%2Fpolyrhythm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deanrad%2Fpolyrhythm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deanrad%2Fpolyrhythm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/deanrad","download_url":"https://codeload.github.com/deanrad/polyrhythm/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249297798,"owners_count":21246478,"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":["actor-model","async","command-object","javascript","observable","polyrhythm","redux","rxjs","typescript"],"created_at":"2024-10-02T09:05:24.862Z","updated_at":"2025-04-17T01:32:00.963Z","avatar_url":"https://github.com/deanrad.png","language":"TypeScript","readme":"[![npm version](https://badge.fury.io/js/polyrhythm.svg)](https://badge.fury.io/js/polyrhythm)[![\u003c4 Kb](https://img.shields.io/badge/gzip%20size-%3C4%20kB-brightgreen.svg)](https://www.npmjs.com/package/polyrhythm)\n[![Travis](https://img.shields.io/travis/deanrad/polyrhythm.svg)](https://travis-ci.org/deanrad/polyrhythm)\n[![Maintainability](https://api.codeclimate.com/v1/badges/a99a88d28ad37a79dbf6/maintainability)](https://codeclimate.com/github/deanius/polyrhythm/maintainability)\n[![TypeScript](https://camo.githubusercontent.com/832d01092b0e822178475741271b049a2e27df13/68747470733a2f2f62616467656e2e6e65742f62616467652f2d2f547970655363726970742f626c75653f69636f6e3d74797065736372697074266c6162656c)](https://github.com/ellerbrock/typescript-badges/)\u003ca href=\"#badge\"\u003e\u003cimg alt=\"code style: prettier\" src=\"https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square\"\u003e\u003c/a\u003e[![twitter link](https://img.shields.io/badge/twitter-@DevRadcliffe-55acee.svg)](https://twitter.com/DevRadcliffe)\n\n# polyrhythm 🎵🎶\n\n`polyrhythm` is a way to avoid async race conditions, particularly those that arise when building UIs, in JavaScript. It can replace Redux middleware like Redux Saga, and is a framework-free library that supercharges your timing and resource-management. And it's under 4Kb.\n\nIts API is a synthesis of ideas from:\n\n- 💜RxJS. Older than Promises, nearly as old as JQuery.\n- 💜Redux-Observable, Redux Saga, Redux Thunk. Async.\n- 💙Macromedia Flash. Mutliple timelines.\n- 💙JQuery. [#on](https://api.jquery.com/on/) and [#trigger](https://api.jquery.com/trigger/).\n\nFor use in a React context, [polyrhythm-react](https://github.com/deanius/polyrhythm-react) exports all in this library, plus React hooks for interfacing with it.\n\n## Installation\n\n```\nnpm install polyrhythm\n```\n\n# What Is It?\n\n`polyrhythm` is a TypeScript library that is a framework-independent coordinator of multiple streams of async using RxJS Observables.\n\nThe goal of `polyrhythm` is to be a centralized control of timing for sync or async operations in your app.\n\nBecause of it's pub-sub/event-bus design, your app remains inherently scalable because originators of events don't know about consumers, or vice versa. If a single subscriber errs out, neither the publisher nor other subscribers are affected. Your UI layer remains simple— its only job is to trigger/originate events. All the logic remains separated from the UI layer by the event bus. Testing of most of your app's effects can be done without going through your UI layer.\n\n`polyrhythm` envisions a set of primitives can compose into beautiful Web Apps and User Experiences more simply and flexibly than current JavaScript tools allow for. All thie with a tiny bundle size, and an API that is delightfully simple.\n\n# Where Can I Use It?\n\nThe framework-independent primitives of `polyrhythm` can be used anywhere. It adds only 3Kb to your bundle, so it's worth a try. It is test-covered, provides types, is production-tested and performance-tested.\n\n---\n\n# Declare Your Timing, Don't Code It\n\nRxJS was written in 2010 to address the growing need for async management code in the world of AJAX. Yet in 2021, it can still be a large impact to the codebase to add `async` to a function declaration, or turn a function into a generator with `function*() {}`. That impact can 'hard-code' in unadaptable behaviors or latency. And relying on framework features (like the timing difference between `useEffect` and `useLayoutEffect`) can make code vulnerable to framework changes, and make it harder to test.\n\n`polyrhythm` gives you 5 concurrency modes you can plug in trivially as configuration parameters, to get the full power of RxJS elegantly.\n\nThe listener option `mode` allows you to control the concurrency behavior of a listener declaratively, and is important for making polyrhythm so adaptable to desired timing outcomes. For an autocomplete or session timeout, the `replace` mode is appropriate. For other use cases, `serial`, `parallel` or `ignore` may be appropriate.\n\nIf async effects like AJAX were represented as sounds, this diagram shows how they might overlap/queue/cancel each other.\n\n\u003ca href=\"https://s3.amazonaws.com/www.deanius.com/ConcurModes2.png\"\u003e\u003cimg height=\"400\" src=\"https://s3.amazonaws.com/www.deanius.com/ConcurModes2.png\"\u003e\u003c/a\u003e\n\nBeing able to plug in a strategy ensures that the exact syntax of your code, and your timing information, are decoupled - the one is not expressed in terms of the other. This lets you write fewer lines, be more direct and declarative, and generally cut down on race conditions.\n\nNot only do these 5 modes handle not only what you'd want to do with RxJS, but they handle anything your users would expect code to do when async process overlap! You have the ease to change behavior to satisfy your pickiest users, without rewriting code - you only have to update your tests to match!\n\n![](https://s3.amazonaws.com/www.deanius.com/async-mode-table.png)\n\nNow let's dig into some examples.\n\n## Example 1: Auto-Complete Input (replace mode)\n\nBased on the original example at [LearnRxjs.io](https://www.learnrxjs.io/learn-rxjs/recipes/type-ahead)...\n\n**Set up an event handler to trigger `search/start` events from an onChange:**\n\n```js\n\u003cinput onChange={e =\u003e trigger('search/start', e.target.value)} /\u003e\n```\n\n**Listen for the `search/results` event and update component or global state:**\n\n```js\nfilter('search/results', ({ payload: results }) =\u003e {\n  setResults(results);\n});\n```\n\n**Respond to `search/start` events with an Observable, or Promise of the ajax request.**\n\u003c\u003c\u003c\u003c\u003c\u003c\u003c HEAD\n\nAssign the output to the `search/results` event, and specify your `mode`, and you're done and race-condition-free!\n\n```js\non(\n  'search/start',\n  ({ payload }) =\u003e {\n    return fetch(URL + payload).then(res =\u003e res.json());\n  },\n  {\n    mode: 'replace',\n    trigger: { next: 'search/results' },\n  }\n);\n```\n\n`mode:replace` does what `switchMap` does, but with readability foremost, and without requiring you to model your app as a chained Observable, or manage Subscription objects or call `.subscribe()` or `.unsubscribe()` explicitly.\n\n[Debounced Search CodeSandbox](https://codesandbox.io/s/debounced-search-polyrhythm-react-w1t8o?file=/src/App.js)\n\n## Example 2: Ajax Cat Fetcher (multi-mode)\n\nBased on an [XState Example](https://dev.to/davidkpiano/no-disabling-a-button-is-not-app-logic-598i) showing the value of separating out effects from components, and how to be React Concurrent Mode (Suspense-Mode) safe, in XState or Polyrhythm.\n\nTry it out - play with it! Is the correct behavior to use `serial` mode to allow you to queue up cat fetches, or `ignore` to disable new cats while one is loading, as XState does? You choose! I find having these options easily pluggble enables the correct UX to be discovered through play, and tweaked with minimal effort.\n\n[Cat Fetcher AJAX CodeSandbox](https://codesandbox.io/s/cat-fetcher-with-polyrhythm-uzjln?file=/src/handlers.js)\n\n## Example 3: Redux Toolkit Counter (multi-mode)\n\nAll 5 modes can be tried in the polyrhythm version of the\n[Redux Counter Example Sandbox](https://codesandbox.io/s/poly-redux-counter-solved-m5cm0)\n\n---\n\n# Can I use Promises instead of Observables?\n\nRecall the auto-complete example, in which you could create a new `search/results` event from either a Promise or Observable:\n\n```js\non('search/start', ({ payload }) =\u003e {\n  // return Observable\n  return ajax.get(URL + payload).pipe(\n    tap({ results } =\u003e results)\n  );\n  // OR Promise\n  return fetch(URL + payload).then(res =\u003e res.json())\n}, {\n mode: 'replace',\n trigger: { next: 'search/results' }\n});\n```\n\nWith either the Promise, or Observable, the `mode: replace` guarantees your autocomplete never has the race-condition where an old result populates after new letters invalidate it. But with an Observable:\n\n- The AJAX can be canceled, freeing up bandwidth as well\n- The AJAX can be set to be canceled implicitly upon component unmount, channel reset, or by another event declaratively with `takeUntil`. And no Abort Controllers or `await` ever required!\n\nYou have to return an Observable to get cancelation, and you only get all the overlap strategies and lean performance when you can cancel. So best practice is to use them - but they are not required.\n\n---\n\n# UI Layer Bindings\n\n`trigger`, `filter` `listen` (aka `on`), and `query` are methods bound to an instance of a `Channel`. For convenience, and in many examples, these bound methods may be imported and used directly\n\n```js\nimport { trigger, on } from 'polyrhythm';\non(...)\ntrigger(...)\n```\n\nThese top-level imports are enough to get started, and one channel is usually enough per JS process. However you may want more than one channel, or have control over its creation:\n\n```js\nimport { Channel } from 'polyrhythm';\nconst channel = new Channel();\nchannel.trigger(...)\n```\n\n(In a React environment, a similar choice exists- a top-level `useListener` hook, or a listener bound to a channel via `useChannel`. React equivalents are discussed further in the [polyrhythm-react](https://github.com/deanius/polyrhythm-react) repo)\n\nTo tie cancelation into your UI layer's component lifecycle (or server-side request fulfillment if in Node), call `.unsubscribe()` on the return value from `channel.listen` or `channel.filter` for any handlers the component set up:\n\n```js\n// at mount\nconst sub = channel.on(...)..\n// at unmount\nsub.unsubscribe()\n```\n\nLastly in a hot-module-reloading environment, `channel.reset()` is handy to remove all listeners, canceling their effects. Include that call early in the loading process to avoid double-registration of listeners in an HMR environment.\n\n# API\n\nA polyrhythm app, sync or async, can be built out of 6 or fewer primitives:\n\n- `trigger` - Puts an event on the event bus, and should be called at least once in your app. Generally all a UI Event Handler needs to do is call `trigger` with an event type and a payload. \u003cbr/\u003eExample — `addEventListener('click', ()=\u003e{ trigger('timer/start') })`\n\n- `filter` - Adds a function to be called on every matching `trigger`. The filter function will be called synchronously in the call-stack of `trigger`, can modify its events, and can prevent events from being further handled by throwing an Error.\u003cbr/\u003eFor metadata — `filter('timer/start', event =\u003e { event.payload.startedAt = Date.now()) })`\u003cbr/\u003eValidation — `filter('form/submit', ({ payload }) =\u003e { isValid(payload) || throw new Error() })`\n\n- `listen` - Adds a function to be called on every matching `trigger`, once all filters have passed. Allows you to return an Observable of its side-effects, and/or future event triggerings, and configure its overlap behavior / concurrency declaratively.\u003cbr/\u003eAJAX: `listen('profile/fetch', ({ payload }) =\u003e get('/user/' + payload.id)).tap(user =\u003e trigger('profile/complete', user.profile))`\n\n- `query` - Provides an Observable of matching events from the event bus. Useful when you need to create a derived Observable for further processing, or for controlling/terminating another Observable. Example: `interval(1000).takeUntil(query('user/activity'))`\n\n## Observable creators\n\n- `after` - Defers a function call into an Observable of that function call, after a delay. This is the simplest way to get a cancelable side-effect, and can be used in places that expect either a `Promise` or an `Observable`. \u003cbr/\u003ePromise — `await after(10000, () =\u003e modal('Your session has expired'))`\u003cbr/\u003eObservable — `interval(1000).takeUntil(after(10000))`\n  `\n- `concat` - Combines Observables by sequentially starting them as each previous one finishes. This only works on Observables which are deferred, not Promises which are begun at their time of creation. \u003cbr/\u003eSequence — `login().then(() =\u003e concat(after(9000, 'Your session is about to expire'), after(1000, 'Your session has expired')).subscribe(modal))`\n\nYou can use Observables from any source in `polyrhythm`, not just those created with `concat` and `after`. For maximum flexibility, use the `Observable` constructor to wrap any async operation - and use them anywhere you need more control over the Observables behavior. Be sure to return a cleanup function from the Observable constructor, as in this session-timeout example.\n\n=======\n\nAssign the output to the `search/results` event, and specify your `mode`, and you're done and race-condition-free!\n\n```js\non(\n  'search/start',\n  ({ payload }) =\u003e {\n    return fetch(URL + payload).then(res =\u003e res.json());\n  },\n  {\n    mode: 'replace',\n    trigger: { next: 'search/results' },\n  }\n);\n```\n\n`mode:replace` does what `switchMap` does, but with readability foremost, and without requiring you to model your app as a chained Observable, or manage Subscription objects or call `.subscribe()` or `.unsubscribe()` explicitly.\n\n[Debounced Search CodeSandbox](https://codesandbox.io/s/debounced-search-polyrhythm-react-w1t8o?file=/src/App.js)\n\n## Example 2: Ajax Cat Fetcher (multi-mode)\n\nBased on an [XState Example](https://dev.to/davidkpiano/no-disabling-a-button-is-not-app-logic-598i) showing the value of separating out effects from components, and how to be React Concurrent Mode (Suspense-Mode) safe, in XState or Polyrhythm.\n\nTry it out - play with it! Is the correct behavior to use `serial` mode to allow you to queue up cat fetches, or `ignore` to disable new cats while one is loading, as XState does? You choose! I find having these options easily pluggble enables the correct UX to be discovered through play, and tweaked with minimal effort.\n\n[Cat Fetcher AJAX CodeSandbox](https://codesandbox.io/s/cat-fetcher-with-polyrhythm-uzjln?file=/src/handlers.js)\n\n## Example 3: Redux Toolkit Counter (multi-mode)\n\nAll 5 modes can be tried in the polyrhythm version of the\n[Redux Counter Example Sandbox](https://codesandbox.io/s/poly-redux-counter-solved-m5cm0)\n\n---\n\n# Can I use Promises instead of Observables?\n\nRecall the auto-complete example, in which you could create a new `search/results` event from either a Promise or Observable:\n\n```js\non('search/start', ({ payload }) =\u003e {\n  // return Observable\n  return ajax.get(URL + payload).pipe(\n    tap({ results } =\u003e results)\n  );\n  // OR Promise\n  return fetch(URL + payload).then(res =\u003e res.json())\n}, {\n mode: 'replace',\n trigger: { next: 'search/results' }\n});\n```\n\nWith either the Promise, or Observable, the `mode: replace` guarantees your autocomplete never has the race-condition where an old result populates after new letters invalidate it. But with an Observable:\n\n- The AJAX can be canceled, freeing up bandwidth as well\n- The AJAX can be set to be canceled implicitly upon component unmount, channel reset, or by another event declaratively with `takeUntil`. And no Abort Controllers or `await` ever required!\n\nYou have to return an Observable to get cancelation, and you only get all the overlap strategies and lean performance when you can cancel. So best practice is to use them - but they are not required.\n\n---\n\n# UI Layer Bindings\n\n`trigger`, `filter` `listen` (aka `on`), and `query` are methods bound to an instance of a `Channel`. For convenience, and in many examples, these bound methods may be imported and used directly\n\n```js\nimport { trigger, on } from 'polyrhythm';\non(...)\ntrigger(...)\n```\n\nThese top-level imports are enough to get started, and one channel is usually enough per JS process. However you may want more than one channel, or have control over its creation:\n\n```js\nimport { Channel } from 'polyrhythm';\nconst channel = new Channel();\nchannel.trigger(...)\n```\n\n(In a React environment, a similar choice exists- a top-level `useListener` hook, or a listener bound to a channel via `useChannel`. React equivalents are discussed further in the [polyrhythm-react](https://github.com/deanius/polyrhythm-react) repo)\n\nTo tie cancelation into your UI layer's component lifecycle (or server-side request fulfillment if in Node), call `.unsubscribe()` on the return value from `channel.listen` or `channel.filter` for any handlers the component set up:\n\n```js\n// at mount\nconst sub = channel.on(...)..\n// at unmount\nsub.unsubscribe()\n```\n\nLastly in a hot-module-reloading environment, `channel.reset()` is handy to remove all listeners, canceling their effects. Include that call early in the loading process to avoid double-registration of listeners in an HMR environment.\n\n# API\n\nA polyrhythm app, sync or async, can be built out of 6 or fewer primitives:\n\n- `trigger` - Puts an event on the event bus, and should be called at least once in your app. Generally all a UI Event Handler needs to do is call `trigger` with an event type and a payload. \u003cbr/\u003eExample — `addEventListener('click', ()=\u003e{ trigger('timer/start') })`\n\n- `filter` - Adds a function to be called on every matching `trigger`. The filter function will be called synchronously in the call-stack of `trigger`, can modify its events, and can prevent events from being further handled by throwing an Error.\u003cbr/\u003eFor metadata — `filter('timer/start', event =\u003e { event.payload.startedAt = Date.now()) })`\u003cbr/\u003eValidation — `filter('form/submit', ({ payload }) =\u003e { isValid(payload) || throw new Error() })`\n\n- `listen` - Adds a function to be called on every matching `trigger`, once all filters have passed. Allows you to return an Observable of its side-effects, and/or future event triggerings, and configure its overlap behavior / concurrency declaratively.\u003cbr/\u003eAJAX: `listen('profile/fetch', ({ payload }) =\u003e get('/user/' + payload.id)).tap(user =\u003e trigger('profile/complete', user.profile))`\n\n- `query` - Provides an Observable of matching events from the event bus. Useful when you need to create a derived Observable for further processing, or for controlling/terminating another Observable. Example: `interval(1000).takeUntil(query('user/activity'))`\n\n## Observable creators\n\n- `after` - Defers a function call into an Observable of that function call, after a delay. This is the simplest way to get a cancelable side-effect, and can be used in places that expect either a `Promise` or an `Observable`. \u003cbr/\u003ePromise — `await after(10000, () =\u003e modal('Your session has expired'))`\u003cbr/\u003eObservable — `interval(1000).takeUntil(after(10000))`\n  `\n- `concat` - Combines Observables by sequentially starting them as each previous one finishes. This only works on Observables which are deferred, not Promises which are begun at their time of creation. \u003cbr/\u003eSequence — `login().then(() =\u003e concat(after(9000, 'Your session is about to expire'), after(1000, 'Your session has expired')).subscribe(modal))`\n\nYou can use Observables from any source in `polyrhythm`, not just those created with `concat` and `after`. For maximum flexibility, use the `Observable` constructor to wrap any async operation - and use them anywhere you need more control over the Observables behavior. Be sure to return a cleanup function from the Observable constructor, as in this session-timeout example.\n\n\u003e \u003e \u003e \u003e \u003e \u003e \u003e 1.2.6 After can defer an Observable\n\n```js\nlisten('user/activity', () =\u003e {\n  return concat(\n    new Observable(notify =\u003e {       // equivalent to after(9000, \"Your session is about to expire\")\n      const id = setTimeout(() =\u003e {\n        notify.next(\"Your session is about to expire\");\n        notify.complete();           // tells `concat` we're done- Observables may call next() many times\n      }, 9000);\n      return () =\u003e clearTimeout(id); // a cancelation function allowing this timeout to be 'replaced' with a new one\n    }),\n    after(1000, () =\u003e \"Your session has expired\"));\n}, { mode: 'replace' });\n});\n```\n\n---\n\n## List Examples - What Can You Build With It?\n\n- The [Redux Counter Example](https://codesandbox.io/s/poly-redux-counter-solved-m5cm0)\n- The [Redux Todos Example](https://codesandbox.io/s/polyrhythm-redux-todos-ltigo)\n- A `requestAnimationFrame`-based [Game Loop](https://codesandbox.io/s/poly-game-loop-xirgs?file=/src/index.js)\n- Seven GUIs Solutions [1-Counter](https://codesandbox.io/s/7guis-1-counter-17pxb) | [2-Temperature](https://codesandbox.io/s/7guis-2-temperature-bnjbf) | [3-Flight](https://codesandbox.io/s/7guis-3-flight-c6wre) | [4-CRUD](https://codesandbox.io/s/7guis-4-crud-7wjut) | [5-Timer](https://codesandbox.io/s/7guis-5-timer-xgop9) _(more info at [7 GUIs](https://eugenkiss.github.io/7guis/tasks))_\n- The [Chat UI Example](https://codesandbox.io/s/poly-chat-imw2z) with TypingIndicator\n- See [All CodeSandbox Demos](https://codesandbox.io/search?refinementList%5Bnpm_dependencies.dependency%5D%5B0%5D=`polyrhythm`\u0026page=1\u0026configure%5BhitsPerPage%5D=12)\n\n# FAQ\n\n**Got TypeScript typings?**\n\nBut of course!\n\n**How large?**\n16Kb parsed size, 4Kb Gzipped\n\n**In Production Use?**\nYes.\n\n**What does it do sync, async? With what Error-Propogation and Cancelability? How does it work?**\n\nSee [The test suite](/test/channel.test.ts) for details.\n\n**How fast is it?**\nNearly as fast as RxJS. The [Travis CI build output](https://travis-ci.org/github/deanius/polyrhythm) contains some benchmarks.\n\n---\n\n# Tutorial: Ping Pong 🏓\n\n\u003cdetails\u003e\n\u003csummary\u003e\nLet's incrementally build the ping pong example app with `polyrhythm`.\n\u003c/summary\u003e\n\n[Finished version CodeSandbox](https://codesandbox.io/s/polyrhythm-ping-pong-r6zk5)\n\n## 1) Log all events, and trigger **ping**\n\n```js\nconst { filter, trigger, log } = require();\n\n// **** Behavior (criteria, fn) ****** //\nfilter(true, log);\n\n// **** Events (type, payload) ****** //\ntrigger('ping', 'Hello World');\n\n// **** Effects ({ type, payload }) ****** //\nfunction log({ type, payload }) {\n  console.log(type, payload ? payload : '');\n}\n// Output:\n// ping Hello World\n```\n\nHere's an app where a `filter` (one of 2 kinds of listeners) logs all events to the console, and the app `trigger`s a single event of `type: 'ping'`.\n\n**Explained:** Filters are functions that run before events go on the event bus. This makes filters great for logging, as you typically need some log output to tell you what caused an error, if an error occurs later. This filter's criteria is simply `true`, so it runs for all events. Strings, arrays of strings, Regex and boolean-functions are also valid kinds of criteria. The filter handler `log` recieves the event as an argument, so we destructure the `type` and `payload` from it, and send it to the console.\n\nWe `trigger` a `ping`, passing `type` and `payload` arguments. This reduces boilerplate a bit compared to Redux' `dispatch({ type: 'ping' })`. But `trigger` will work with a pre-assembled event too. Now let's play some pong..\n\n## 2) Respond to **ping** with **pong**\n\nIf we just want to respond to a **ping** event with a **pong** event, we could do so in a filter. But filters should be reserved for synchronous side-effect functions like logging, changing state, or dispatching an event/action to a store. So let's instead use `listen` to create a **Listener**.\n\n```js\nconst { filter, listen, log, trigger } = require('polyrhythm');\n\nfilter(true, log);\nlisten('ping', () =\u003e {\n  trigger('pong');\n});\n\ntrigger('ping');\n// Output:\n// ping\n// pong\n```\n\nWe now have a **ping** event, and a **pong** reply. Now that we have a game, let's make it last longer.\n\n## 3) Return Async From an Event Handler\n\nNormally in JavaScript things go fine—until we make something async. But `polyrhythm` has a solution for that, a simple utility function called `after`. I like to call `after` _\"the `setTimeout` you always wanted\"_.\n\nLet's suppose we want to trigger a **pong** event, but only after 1 second. We need to define the **Task** that represents \"a triggering of a `pong`, after 1 second\".\n\n```js\nconst { filter, listen, log, after, trigger } = require('polyrhythm');\n\nfilter(true, log);\nlisten('ping', () =\u003e {\n  return after(1000, () =\u003e trigger('pong'));\n});\n\ntrigger('ping');\n// Output: (1000 msec between)\n// ping\n// pong\n```\n\nIn plain, readable code, `after` returns an Observable of the function call you pass as its 2nd argument, with the delay you specify as its 1st argument. Read aloud, it says exactly what it does: _\"After 1000 milliseconds, trigger `pong`\"_\n\n**TIP:** An object returned by `after` can be directly `await`-ed inside an async functions, as shown here:\n\n```js\nasync function sneeze() {\n  await after(1000, () =\u003e log('Ah..'));\n  await after(1000, () =\u003e log('..ah..'));\n  await after(1000, () =\u003e log('..choo!'));\n}\n```\n\n**IMPORTANT:** All Observables, including those returned by `after`, are lazy. If you fail to return them to `polyrhythm`, or call `toPromise()`, `then()`, or `subscribe()` on them, they will not run!\n\nBut back to ping-pong, let's respond both to **ping** and **pong** now...\n\n## 4) Ping-Pong forever!\n\nFollowing this pattern of adding listeners, we can enhance the behavior of the app by adding another listener to `ping` it right back:\n\n```js\nconst { filter, listen, log, after, trigger } = require('polyrhythm');\n\nfilter(true, log);\nlisten('ping', () =\u003e {\n  return after(1000, () =\u003e trigger('pong'));\n});\nlisten('pong', () =\u003e {\n  return after(1000, () =\u003e trigger('ping'));\n});\n\ntrigger('ping');\n// Output: (1000 msec between each)\n// ping\n// pong\n// ping\n// pong  (etc...)\n```\n\nIt works! But we can clean this code up. While we could use a Regex to match either **ping** or **pong**, a string array does the job just as well, and is more grep-pable. We'll write a `returnBall` function that can `trigger` either **ping** or **pong**, and wire it up.\n\n```js\nfilter(true, log);\nlisten(['ping', 'pong'], returnBall);\n\ntrigger('ping');\n\nfunction returnBall({ type }) {\n  return after(1000, () =\u003e trigger(type === 'ping' ? 'pong' : 'ping'));\n}\n```\n\nNow we have an infinite game, without even containing a loop in our app! Though we're dispensing with traditional control structures like loops, we're also not inheriting their inability to handle async, so our app's code will be more flexible and readable.\n\nIn ping-pong, running forever may be what is desired. But when it's not, or when parts of the app are shutdown, we'll want to turn off listeners safely.\n\n## 5) Shutdown Safely (Game Over!)\n\nWhile each listener can be individually shut down, when it's time to shut down the app (or in Hot Module Reloading scenarios), it's good to have a way to remove all listeners. The `reset` function does just this. Let's end the game after 4 seconds, then print \"done\".\n\n```js\nconst { filter, listen, log, after, reset, trigger } = require('polyrhythm');\n\nfilter(true, log);\nlisten(['ping', 'pong'], returnBall);\n\ntrigger('ping');\nafter(4000, reset).then(() =\u003e console.log('Done!'));\n\n//Output:\n// ping\n// pong\n// ping\n// pong\n// Done!\n```\n\nNow that's a concise and readable description!.\n\nThe function `after` returned an Observable of calling `reset()` after 4 seconds. Then we called `then()` on it, which caused `toPromise()` to be invoked, which kicked off its subscription. And we're done!\n\n**TIP:** To shut down an individual listener, `listen` returns a **Subscription** that is disposable in the usual RxJS fashion:\n\n```js\nfilter(true, log);\nlisten('ping', () =\u003e {\n  return after(1000, () =\u003e trigger('pong'));\n});\nconst player2 = listen('pong', () =\u003e {\n  return after(1000, () =\u003e trigger('ping'));\n});\n\ntrigger('ping');\nafter(4000, () =\u003e player2.unsubscribe()).then(() =\u003e console.log('Done!'));\n\n//Output:\n// ping\n// pong\n// ping\n// pong\n// Done!\n```\n\nCalling `unsubscribe()` causes the 2nd Actor/Player to leave the game, effectively ending the match, and completing the ping-pong example!\n\n\u003c/details\u003e\n\n---\n\n# Further Reading\n\nThe following were inspiring principles for developing `polyrhythm`, and are definitely worth reading up on in their own right:\n\n- [Command Object Pattern](https://en.wikipedia.org/wiki/Command_pattern#:~:text=In%20object%2Doriented%20programming%2C%20the,values%20for%20the%20method%20parameters.)\n- [Pub Sub Pattern](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern)\n- [Actor Model](https://en.wikipedia.org/wiki/Actor_model)\n- [Event Sourcing / CQRS](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation#Command_query_responsibility_segregation)\n- [Flux Standard Action](https://github.com/redux-utilities/flux-standard-action) | [Redux](https://redux.js.org)\n- [TC39 Observable proposal](https://github.com/tc39/proposal-observable)\n- [ReactiveX](http://reactivex.io/)\n- [RxJS Concurrency operators](https://rxjs-dev.firebaseapp.com/)\n- [Turning the Database Inside Out (Samza/Kafka)](https://www.confluent.io/blog/turning-the-database-inside-out-with-apache-samza/)\n- [Svelte](https://svelte.dev/)\n- [Elm](https://elm-lang.org/)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeanrad%2Fpolyrhythm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdeanrad%2Fpolyrhythm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeanrad%2Fpolyrhythm/lists"}