{"id":13566863,"url":"https://github.com/deanrad/omnibus-rxjs","last_synced_at":"2025-04-22T22:17:56.877Z","repository":{"id":39579477,"uuid":"397653242","full_name":"deanrad/omnibus-rxjs","owner":"deanrad","description":"An 8Kb Event Bus made with RxJS. Like Redux Middleware, without Redux","archived":false,"fork":false,"pushed_at":"2022-09-20T15:15:29.000Z","size":1297,"stargazers_count":13,"open_issues_count":2,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-04-22T22:17:47.665Z","etag":null,"topics":["kafka","observables","redux","rxjs"],"latest_commit_sha":null,"homepage":"https://deanius.gitbook.io/omnibus-documentation/","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":null,"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":"2021-08-18T15:39:33.000Z","updated_at":"2024-09-16T07:21:14.000Z","dependencies_parsed_at":"2022-07-11T02:38:33.206Z","dependency_job_id":null,"html_url":"https://github.com/deanrad/omnibus-rxjs","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deanrad%2Fomnibus-rxjs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deanrad%2Fomnibus-rxjs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deanrad%2Fomnibus-rxjs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deanrad%2Fomnibus-rxjs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/deanrad","download_url":"https://codeload.github.com/deanrad/omnibus-rxjs/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250331819,"owners_count":21413103,"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":["kafka","observables","redux","rxjs"],"created_at":"2024-08-01T13:02:18.290Z","updated_at":"2025-04-22T22:17:56.734Z","avatar_url":"https://github.com/deanrad.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"[![Travis CI](https://api.travis-ci.com/deanrad/omnibus-rxjs.svg?token=jDxJBxYkkXVxwqfuGjmx\u0026branch=master\u0026status=passed)](https://travis-ci.com/deanrad/omnibus-rxjs)\n![Code Coverage](https://shields.io/badge/coverage-100%25-brightgreen)\n[![Maintainability](https://api.codeclimate.com/v1/badges/f7c14c5a3bbbf0d803cc/maintainability)](https://codeclimate.com/github/deanrad/omnibus-rxjs/maintainability)\n\n# omnibus-rxjs\n\n## What Is It?\n\nAn Event Bus for simplifying front-end code, especially in VanillaJS and React codebases. Can serve as its own framework, or an add-on, run in Node, or Deno, and maintain decoupling from frameworks, or downstream services.\n\n## How to Get It?\n\n`npm install omnibus-rxjs`\n\nDeno: Coming Soon!\n\n## How Big Is It?\n\nOnly 8Kb minified, gzipped\n\n## What Front-End problems does it help with?\n\n- Keep components and services testable—since they're specified only in terms of messages they send or respond to - no mocking required!\n- Don't need to prop-drill, lift state, or introduce Contexts to do inter-component communication; sharing the bus is sufficient.\n- Code UX to handle all edge-cases around API/service communication, by depending only on the messages. Even if those services aren't built yet!\n- Keep memory footprint small, and prevent bundle bloat by allowing functionality to load/unload at runtime.\n\nAnd many more - see How Can I Explain This To My Team.\n\n## Usage with React\n\n```ts\nimport { bus, CounterIncrement, useWhileMounted } from \"./events/\"\nconst CounterDisplay = () =\u003e {\n  const [count, setCount] = useState(0);\n  useWhileMounted(() =\u003e {\n    return bus.listen(CounterIncrement.match, () =\u003e {\n      setCount(c =\u003e c+1))\n    })\n  })\n}\n```\n\nThis example invokes a React state-setter each time an event matching `CounterIncrement` is trigger-ed onto the bus. `bus.listen` returns an RxJS `Subscription` object, and the wrapping of it in `useWhileMounted` allows the listener to be removed upon component unmounting.\n\nIn an entirely un-coupled component, anywhere in the app, a component (or test framework) will trigger those actions:\n\n```ts\nimport { bus, CounterIncrement } from './events'\nconst CounterButton = () =\u003e {\n  return \u003cbutton onClick={() =\u003e trigger(CounterIncrement())}\u003e\n}\n```\n\nAll that's needed to connect them, is mount each of them - in no particular relation to each other, and sharing no props or state:\n\n```jsx\n\u003cApp\u003e\n  \u003cCounterDisplay /\u003e\n  \u003cCounterButton /\u003e\n\u003c/App\u003e\n```\n\n### Lifecycle\n\n`useWhileMounted` can ensure your effects do not outlive the components that initiate them. This is a good default, and enabled by returning Observables from handlers always. However, if cancelability is not desired, (such as when a response is still desired) simply return a Promise instead, and Omnibus will be unable to cancel it.\n\n```ts\nfunction useWhileMounted(subsFactory: () =\u003e Subscription) {\n  useEffect(() =\u003e {\n    const sub = subsFactory();\n    return () =\u003e sub?.unsubscribe();\n  }, []);\n}\n```\n\n### Testing\n\nNote how the specs read for each component:\n\n```\ndescribe: CounterButton\n  it: triggers a CounterIncrement event when clicked\n\ndescribe: CounterDisplay\n  it: increments its number by 1 upon a CounterIncrement event\n```\n\nWith that specification, no test-framework specific mocks need to be written — `query` can be used to assert what `CounterButton` does, and a test need only `trigger` and examine the output of `CounterDisplay`. No complicated, nested `jest.mock` calls required. Bonus: you can animate your Storybook stories by performing a series of `trigger` calls in your stories.\n\n# Example Applications\n\nThere is [TodoMVC](https://codesandbox.io/s/todos-omnibus-rtkquery-mjjwn). The Redux Toolkit [Async Counter](https://codesandbox.io/s/omnibus-async-counter-0q95qz). An RxJS-style [typeahead search](https://codesandbox.io/s/createobservableservice-6zy19). And a rework of the XState Dog Fetcher known as [Cat Fetcher](https://codesandbox.io/s/cat-fetcher-with-omnibus-kr3yw)\n\nThe [7 GUIs](https://eugenkiss.github.io/7guis/) are a series of UX challenges which range from a simple counter to a highly dynamic spreadsheet with formulae and cell references.\n\n- [1-Counter](https://codesandbox.io/s/7guis-1-counter-glh0cm)\n- [2-Temperature](https://codesandbox.io/s/7guis-2-temperature-n36pf?file=/src/index.tsx)\n- 3-Flight Booker\n- 4-Timer\n- [5-CRUD](https://codesandbox.io/s/7guis-5-crud-omnibus-z99bd0)\n- [6-Circles](https://codesandbox.io/s/7guis-6-circles-omnibus-bnfsm)\n- [7-Cells (command-line)](https://github.com/deanrad/omnibus-rxjs/tree/master/example/7guis-cells)\n\nOmnibus solves each challenge, maintaining a uniform, testable architectural style across each one. Other example apps have included IOT, Animation, WebAudio, WebSockets and many more.\n\n# API\n\nThe Omnibus API is intentionally simple. Since it has been in use for \u003e4 years (since 2017), its core APIs are stable.\n\n---\n\n\u003e **Constructor**: `new Omnibus\u003cEType\u003e()`\n\nDeclares the event bus, and the (super)-type of events it can carry. When used with a library like [Redux Toolkit](), or [`typescript-fsa`](), this will be:\n\n`export const bus = new Omnibus\u003cAction\u003e()`\n\n---\n\n\u003e **trigger**: `bus.trigger\u003cSubType extends EType\u003e(event)`\n\nTriggers an action to the event bus, so that listeners may handle it after passing all pre-processors (guards, filters, and spies). _Triggering has no performance cost if there are no listeners_.\n\nAs an example: if you have logging that should only occur in lower environments, the calls to `bus.trigger` can be left in place throughout your app and logging listeners only attached in lower environments. Compare this to actual `console.log` statements which must be removed.\n\nNo type-annotation is needed at call-time to ensure typesafety of triggered actions.\n\n```js\n// CounterIncrement triggers a subtype of Action onto the bus\nhandleClick={((e) =\u003e bus.trigger(CounterIncrement()))}\n```\n\n---\n\n\u003e **query**: `bus.query(predicate: (EType =\u003e boolean))`\n\nFrom a testing perspective, `query` is a way to assert that a `trigger` was called.\n\nMore generally, `query` allows you to get a subset of actions of the bus as an RxJS Observable. This allows you create a 'derived stream' to detect certain conditions. A simplistic form of rate limiting could be:\n\n```ts\n// Do something when 5 or more bus events occur in one second\nconst rateLimitViolations = bus\n  .query(() =\u003e true)\n  .pipe(\n    bufferTime(1000),\n    filter((buffer) =\u003e buffer.length \u003e= 5)\n  );\nrateLimitViolations.subscribe(() =\u003e {\n  // let this be handled by listeners\n  bus.trigger(RateLimitViolation());\n  console.log('Slow down!');\n});\n```\n\nIssues resulting from race conditions could be detected and fixed by using `query` to run a corrective action.\n\nYou can get a Promise for the first result of a `query`, using RxJS' `firstValueFrom`:\n\n```ts\nimport { firstValueFrom } from 'rxjs';\nfirstValueFrom(rateLimitViolations).then(() =\u003e console.log('Game over!'));\n```\n\n---\n\n\u003e **reset**: `bus.reset()`\n\nReturns the bus to a state where there are no listeners. Any Observables returned by `query` will complete upon a `reset`. Any listeners will be unsubscribed. If the listeners were defined to support cancelation of their effects (if they return cancelable Observables vs uncancelable Promises), their effects are canceled and resources freed up immediately. See Cancelation for more.\n\nIn a Hot-Module-Reloading environment where a bus instance may get the same listeners attached multiple times, adding a call to `reset` can prevent double-listenings.\n\n```ts\nbus.reset(); // Be HMR-friendly\n```\n\n## Error Handling\n\n---\n\n\u003e **errors**: `bus.errors.subscribe(handler)`\n\nIf a listener throws an uncaught error (its `observer` does not have an `error` callback):\n\n- The `error` will appear on the `bus.errors` Observable.\n- The listener will be unsubscribed/terminated.\n\nFor full transparency, the following code will show all events on the bus, and any errors that occur:\n\n```ts\nconst bus = new Omnibus\u003cany\u003e();\nbus.spy((e) =\u003e console.log(e));\nbus.errors.subscribe((e) =\u003e console.error(e));\n```\n\nThis error handling differs from RxJS in two ways:\n\n- The code that called `bus.trigger()` and initiated the error will NOT see the exception. This keeps triggerering components from being sensitive to listener exceptions, allowing those components to reach a 'done' state, even when new listeners are added.\n\n- The bus' internal `Subject` will continue to handle events, and keep other listeners alive. No `hostReportError` will be called.\n\nIn short, the bus prefers to 'blow a fuse' on a single listener, than to fail entirely, or raise an error or Promise rejection to the top level of the app.\n\nIn contrast to listener errors, Guards, Filters and Spies that throw errors will propogate up to the code that called `trigger`. Use this to perform event validation or otherwise tell the triggerer that the event it sent will not be handled. See Sync Handlers for more.\n\n---\n\n## Async Handlers\n\n\u003e **listen**: `bus.listen(matcher, handler, observer)`\n\n\u003e **listenQueueing**: `bus.listenQueueing`\n\n\u003e **listenSwitching**: `bus.listenSwitching`\n\n\u003e **listenBlocking**: `bus.listenBlocking`\n\nEach `bus.listen` method variant takes the same function arguments (explained shortly), and returns a Subscription. This subscription can be thought of as an Actor in the Actor model.\n\nThe purpose of using one `listen` variant over another - say `listenQueueing` instead of `listen`, is to specify what the listener does if it already is executing a handling, and a new event comes in. Each method corresponds to an RxJS operator. More detail below in Concurrency.\n\n### **Arguments**\n\nThe two required arguments to create a listener are:\n\n- `matcher` - A predicate function. Each event of the bus is run through each listener's predicate function. If the matcher returns `true` then the bus invokes that listener's handler.\n- `handler` - A function to do some work when a matching event appears on the bus. The handler recieves the event as its argument. It can perform the work:\n  - Synchronously\n  - Immediately, by returning a Promise\n  - Deferred, by returning a Promise-returning function\n  - Deferred, and cancelable by returning an Observable\n\nThe most powerful and performant of these is the Observable, whose cancelability prevents resource leaks and helps tame race conditions. **Note:** You do not need to call `subscribe()` when returning an Observable- Omnibus does that automatically on any Observable you return. When returning a Promise, there's no need to `await` it or declare the handler `async`.\n\n```ts\n// Sync\nbus.listen(matcher, (e) =\u003e console.log('event: ', e));\n\n// Immediate Promise\nbus.listen(matcher, (e) =\u003e fetch(url).then((res) =\u003e res.json()));\n\n// Deferred Promise (needed if you want to queue handlings)\nbus.listen(matcher, (e) =\u003e () =\u003e fetch(url).then((res) =\u003e res.json()));\n\n// Observable (cancelable w/o AbortController!)\nbus.listen(matcher, (e) =\u003e ajax.getJSON(url));\n```\n\n### **Concurrency**\n\nBy default, a listener will perform its handlings ASAP - corresponding to `mergeMap` mode in RxJS. However, race conditions or resource usage often demand switching strategies. Omnibus makes this trivial - just call a different `listen` variant with the same arguments. The async concurrency strategy (**How**) is entirely decoupled from the criteria for triggering (**When**), and the async behavior definition (**What**). This allows those changes to the UX that really make a difference to users, without requiring large code changes:\n\n| Variant           | RxJS operator | Use Cases                    |\n| ----------------- | ------------- | ---------------------------- |\n| `listen`          | `mergeMap`    | Independent \"Like\" buttons   |\n| `listenQueueing`  | `concatMap`   | File upload / analytics      |\n| `listenSwitching` | `switchMap`   | Autocomplete/session timeout |\n| `listenBlocking`  | `exhaustMap`  | Form submission              |\n\nThese variants suffice for 99.9% of the use cases you'll encounter in web development. For custom handling (example: a maximum of 2 in progress handlings), pass a custom operator of your own design as the last argument to `listen`. See `toggleMap` for an example.\n\n### **Handler Lifecycle Events**\n\nFor any individual handling, there are events of interest - such as when it starts, errors, or produces a value successfully. The final, optional argument in constructing a listener is `observer`— an object with any of these callbacks:\n\n- `error(e)` - A Promise `reject` or Observable `error` occurred.\n- `complete()` - A Promise `resolve` or Observable `complete` occurred.\n- `next(value)` - Conveys the resolved Promise value, or an Observable `next` value.\n- `subscribe()` - The deferred Promise was begun, or the Observable was subscribed to.\n- `unsubscribe()` - The Observable was unsubscribed before it completed. Aborted Promises are handled as `error` instead.\n\nIf you are using the `observer` simply to trigger further events (like Redux Toolkit's `createAsyncThunk` but with different names), you can create a triggering Observer out of action creators using `bus.observeWith`:\n\n```ts\n// Import Action Creators for the lifecycle events\nimport { SearchRequest, SearchResult, SearchComplete SearchError } from './events';\n\n// And trigger these events as the handler runs\nbus.listen(\n  SearchRequest.match,\n  (req) =\u003e fetch(`https://url?req=${req}`),\n  bus.observeWith({\n    next: SearchResult,\n    complete: SearchComplete,\n    error: SearchError\n  })\n);\n```\n\nSee [`createObservableService`](https://codesandbox.io/s/createobservableservice-6zy19) for a utility that resembles `createAsyncThunk`, but without Redux or middleware, saving bundle size.\n\n### **Unregistering a Listener**\n\nTo unregister the listener, simply call `unsubscribe()` on the return value. Any work being done by a listener will be canceled when the listener's subscription is unsubscribed, or if `bus.reset()` is called.\n\n```ts\nconst sub = bus.listen(when, () =\u003e work); // sets up\nbus.trigger(startsWork); // begins work\nsub.unsubscribe(); // ends, including work\n```\n\n---\n\n## Pre-Processors/Sync Handlers: Guards, Filters, Spies\n\nThe bus allows for various forms of pre-processors - functions which run on every `trigger`-ed action, before any listener sees the action. These are not intended to begin any async process, but more commonly to:\n\n- Throw an exception to the caller of `bus.trigger()` (Listeners, by design do not do this - see Error Handling)\n- Validate, sign, timestamp or otherwise modify the event before any listener can see it.\n- Call a state setter (as in React useState), or dispatch an action (as in useReducer)\n- `console.log` or write to another log synchronously.\n\n\u003e **guard**: `bus.guard(matcher, handler)`\n\nGuard functions are run in the callstack of the triggerer. Their return value is unimportant, but may throw an exception to prevent further processing.\n\n```js\n// Prevent negative increments\nbus.guard(CounterIncrement.match, ({ payload: { increment = 0 } }) =\u003e {\n  if (increment \u003c 0) throw new Error('Cant go backward');\n});\nbus.trigger(CounterIncrement(-1)); // throws\n```\n\nA fun debugging technique is to open a debugger upon an action of your choosing, to find out where in the code it was triggered (since `grep` doesnt always find dynamic event creation):\n\n```js\nbus.guard(CounterIncrement.match, () =\u003e {\n  debugger; /* i see you! */\n});\n```\n\n\u003e **filter**: `bus.filter(matcher, handler)`\n\nIf you are intending to change the contents of an event on the bus, a handler passed to `filter` is the place to do it. Either mutate the event directly in the handler, or if you prefer immutability, return a new event. This is useful to centralize functionality like timestamps, making it not the responsibility of the trigger-er.\n\n```ts\nbus.filter(CounterIncrement.match, (e) =\u003e {\n  e.createdAt = Date.now()\n}\n```\n\nIf an event requires further authorization, a `filter` may substitute an authenitcation event in its place:\n\n```ts\nbus.filter(CounterIncrement.match, (event) =\u003e {\n  return RequestAuth({ attempted: event })\n}\nbus.listen(RequestAuth.match, ({payload: { attempted }}) =\u003e {\n  if (window.sessionId) {\n    perform(attempted)\n  }\n})\n```\n\nIf a set of timing conditions requires that an event be disregarded by all downstream handlers, a filter can change its event to not be seen by those handlers:\n\n```ts\n// Increments will be missed by handlers matching on type\nbus.filter(CounterIncrement.match, (e) =\u003e {\n  if (count \u003e 99) {\n    e.type = \"__ignored__\" + e.type\n  }\n}\n```\n\n\u003e **spy**: `bus.spy(handler)`\n\nA spy handler runs synchronously for all runtime events, just before the listeners. The results of any `filter` is visible to any `spy`. Spies should not mutate events, and as long as they don't, they will see exactly what each listener will see.\n\n---\n\n# How Can I Explain Why We Should Use This to My Team?\n\nThe main benefits of Omnibus are:\n\n- Allows you to architect your application logic around events of interest to your application, not around volatile or error-prone framework-specific APIs.\n- Provides an execution container for typesafe, leak-proof async processes with reliable concurrency options to squash race conditions and prevent resource leaks.\n\nTo the first point - framework-specific issues like \"prop-drilling\" and \"referential instability\" disappear when an event bus transparently connects components anywhere in the tree through a single, stable bus instance.\n\nTo the reliability point - just as XState is a predictable, safe, leak-proof state-container, Omnibus is that for async processes, because it uses the \u003e10 year old, tested options of RxJS: Observables and concurrency operators.\n\nWith Omnibus inside React, you can:\n\n- Keep components and services testable—simply specify them in terms of messages they send or respond to, and listen - no mocking required!\n- Prevent the need to prop-drill, lift state, or introduce Contexts to do inter-component communication; sharing the bus is sufficient.\n- Develop UX to handle all edge-cases around API/service communication, even if those services aren't built yet, by decoupling from them with the event bus!\n- Keep memory footprint small, and prevent bundle bloat by allowing functionality to load/unload at runtime.\n\nWith Omnibus over RxJS, you can:\n\n- Compose your app one listener/handler at a time, never building a giant, unreadable chain.\n- Do little-to-no management of `Subscription` objects\n- Preserve readability of operator code: `concatMap` =\u003e `listenQueueing`\n- Type `pipe()` and `import ... from 'rxjs/operators'` less\n\nYou can start with Omnibus with no RxJS logic at all - just handlers returning Promises. Then as you require capabilities that Observables offer—like cancelation— you can change what those handlers return. _Leaving the rest of your app unchanged!_ No `async/await` is required. And you need not mix several types of async code like: middlewares, async/await, Promise chaining and framework-specific APIs. Just use events and listeners.\n\nIn short - the kinds of upgrades one must do in web development, such as migrating code from uncancelable to cancelable, from REST endpoint to Web Socket, are made easy with Omnibus. And the UX can be made tight and responsive against any downstream behavior because of its modular, decoupled nature.\n\n# Inspirations, References\n\n- RxJS\n- Redux-Observable\n- XState\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeanrad%2Fomnibus-rxjs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdeanrad%2Fomnibus-rxjs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeanrad%2Fomnibus-rxjs/lists"}