{"id":13567065,"url":"https://github.com/ryardley/ts-bus","last_synced_at":"2025-04-05T18:06:49.172Z","repository":{"id":34931780,"uuid":"191695440","full_name":"ryardley/ts-bus","owner":"ryardley","description":"A lightweight JavaScript/TypeScript event bus to help manage your application architecture.","archived":false,"fork":false,"pushed_at":"2023-04-08T12:58:27.000Z","size":19284,"stargazers_count":139,"open_issues_count":41,"forks_count":8,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-03-29T17:08:34.486Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/ryardley.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","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":"2019-06-13T05:19:27.000Z","updated_at":"2025-03-14T17:07:21.000Z","dependencies_parsed_at":"2024-06-18T15:28:52.794Z","dependency_job_id":"fb02e14e-31a4-466c-88ad-04bdf02c8351","html_url":"https://github.com/ryardley/ts-bus","commit_stats":{"total_commits":161,"total_committers":3,"mean_commits":"53.666666666666664","dds":0.0496894409937888,"last_synced_commit":"19c5426374ddab961bab54176dc41955f41914ab"},"previous_names":[],"tags_count":31,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryardley%2Fts-bus","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryardley%2Fts-bus/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryardley%2Fts-bus/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryardley%2Fts-bus/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ryardley","download_url":"https://codeload.github.com/ryardley/ts-bus/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247378141,"owners_count":20929296,"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":[],"created_at":"2024-08-01T13:02:22.772Z","updated_at":"2025-04-05T18:06:49.153Z","avatar_url":"https://github.com/ryardley.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"logo.png\" width=\"100\" height=\"100\"/\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003e\nts-bus\n\u003c/h1\u003e\n\n#### A lightweight TypeScript event bus to help manage your application architecture\n\n[![Build Status](https://travis-ci.org/ryardley/ts-bus.svg?branch=master)](https://travis-ci.org/ryardley/ts-bus)\n[![codecov](https://codecov.io/gh/ryardley/ts-bus/branch/master/graph/badge.svg)](https://codecov.io/gh/ryardley/ts-bus)\n[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ryardley/ts-bus/blob/master/LICENSE)\n\n### Example\n\n```ts\nimport { EventBus, createEventDefinition } from \"ts-bus\";\n\n// Define Event\nexport const someEvent = createEventDefinition\u003c{ url: string }\u003e()(\"SOME_EVENT\");\n\n// Create bus\nconst bus = new EventBus();\n\n// Subscribe\nbus.subscribe(someEvent, event =\u003e {\n  alert(event.payload.url);\n});\n\n// Publish\nbus.publish(someEvent({ url: \"https://github.com\" }));\n```\n\n### Rationale\n\nWe want to write loosely coupled highly cohesive applications and one of the best and easiest ways to do that is to use an event bus as a management layer for our applications.\n\nThis is the kind of thing that you could use effectively in most applications.\n\nFor my purposes I wanted a system that:\n\n- Is framework agnostic can support Vue, React or Angular.\n- Could enable micro-frontends / microlithic architecture.\n- Can easily use React hooks to reduce state in the case of React.\n- Does not conflate eventing with state management.\n- Has really good TypeScript support.\n\n### Alternatives\n\n- Redux - conflates state management with eventing and causes complexity around async as a result. Redux has a highly invasive syntax that is difficult to remove or abstract out of an application. React comes with state management out of the box these days anyway. See my article [\"Life after Redux\"](https://itnext.io/life-after-redux-21f33b7f189e?source=friends_link\u0026sk=a2566ae4b3b28797505a1295d70392fe)\n- RxJS - could make a great event bus but feels too heavy handed for use with many projects.\n- Node `events` - is a little too much API for what I need here. This lib actually decorates the `EventEmitter2` package. In the future I may remove it to become dependency free.\n\n## Upgrading to v3\n\nVersion 3 includes a couple of breaking changes to the react extensions. Now both `useBusState` and `useBusReducer` return tuples the same as their React equivalents.\n\n```ts\n// Old \nconst state = useBusReducer(/* ... */);\n\n// New \nconst [state, dispatch] = useBusReducer(/* ... */);\n```\n\n```ts\n// Old\nconst count = useBusState(0, eventCreator);\n\n// New\nconst useState = useBusState.configure(eventCreator);\n\nconst [count, setCount] = useState(0);\n```\n\nAlso the configuration for useBusState has changed.\n\nSee [useBusReducer](#useBusReducer), [useBusState](#useBusState).\n\n## Installation\n\nUse your favourite npm client to install ts-bus. Types are included automatically.\n\nNpm:\n\n```bash\nnpm install ts-bus\n```\n\nYarn:\n\n```bash\nyarn add ts-bus\n```\n\n## Example applications\n\n[With Redux Devtools](examples/with-redux-dev-tools).\n\n## Usage\n\n### Create a bus\n\nCreate your EventBus globally somewhere:\n\n```ts\n// bus.ts\nimport { EventBus } from \"ts-bus\";\nexport const bus = new EventBus();\n```\n\n### Declare events\n\nNext create some Events:\n\n```ts\n// events.ts\nimport { createEventDefinition } from \"ts-bus\";\n\nexport const taskCreated = createEventDefinition\u003c{\n  id: string;\n  listId: string;\n  value: string;\n}\u003e()(\"task.created\");\n\nexport const taskLabelUpdated = createEventDefinition\u003c{\n  id: string;\n  label: string;\n}\u003e()(\"task.label.updated\");\n```\n\nNotice `createEventDefinition()` will often be called with out a runtime check argument and it returns a function that accepts the event type as an argument. Whilst possibly a tiny bit awkward, this is done because it is [the only way we can allow effective discriminated unions](https://github.com/ryardley/ts-bus/issues/9). See [switching on events](#switching-on-events-and-discriminated-unions).\n\n### Runtime payload checking\n\nYou can also provide a predicate to do runtime payload type checking in development. This is useful as a sanity check if you are working in JavaScript:\n\n```js\nimport p from \"pdsl\";\n\n// pdsl creates predicate functions\nconst isLabel = p`{\n  id: string,\n  label: string,\n}`;\n\nexport const taskLabelUpdated = createEventDefinition(isLabel)(\n  \"task.label.updated\"\n);\n\ntaskLabelUpdated({ id: \"abc\" }); // {\"id\":\"abc\"} does not match expected payload.\n```\n\nThese warnings are suppressed in production.\n\n### Subscribing\n\n```ts\nimport { taskLabelUpdated, taskCreated } from \"./event\";\nimport { bus } from \"./bus\";\n\n// You can subscribe using the event creator function\nbus.subscribe(taskLabelUpdated, event =\u003e {\n  const { id, label } = event.payload; // Event is typed\n  doSomethingWithLabelAndId({ id, label });\n});\n```\n\n### Unsubscribing\n\nTo unsubscribe from an event use the returned unsubscribe function.\n\n```ts\nconst unsubscribe = bus.subscribe(taskLabelUpdated, event =\u003e {\n  // ...\n});\n\nunsubscribe(); // removes event subscription\n```\n\n### Subscribing with a type string\n\nYou can use the event type to subscribe.\n\n```ts\nbus.subscribe(\"task.created\", event =\u003e {\n  // ...\n});\n```\n\nOr you can use [wildcards](#wildcard-syntax):\n\n```ts\nbus.subscribe(\"task.**\", event =\u003e {\n  // ...\n});\n```\n\n### Subscribing with a predicate function\n\nYou can also subscribe using a predicate function to filter events.\n\n```ts\n// A predicate\nfunction isSpecialEvent(event) {\n  return event.payload \u0026\u0026 event.payload.special;\n}\n\nbus.subscribe(isSpecialEvent, event =\u003e {\n  // ...\n});\n```\n\nYou may find [pdsl](https://github.com/ryardley/pdsl) a good fit for creating predicates.\n\n### Subscription syntax\n\nAs you can see above you can subscribe to events by using the `subscribe` method of the bus.\n\n```ts\nconst unsubscriber = bus.subscribe(\u003cstring|eventCreator|predicate\u003e, handler);\n```\n\nThis subscription function can accept a few different options for the first argument:\n\n- A `string` that is the specific event type or a wildcard selector eg. `mything.**`.\n- An `eventCreator` function returned from `createEventDefinition\u003cPayloadType\u003e()(\"myEvent\")`\n- A `predicate` function that will only subscribe to events that match the predicate. Note the predicate function matches the entire `event` object not just the payload. Eg. `{type:'foo', payload:'foo'}`\n\nThe returned `unsubscribe()` method will unsubscribe the specific event from the bus.\n\n### Publishing events\n\nNow let's publish our events somewhere\n\n```ts\n// publisher.ts\nimport { taskLabelUpdated, taskCreated } from \"./events\";\nimport { bus } from \"./bus\";\n\nfunction handleUpdateButtonClicked() {\n  bus.publish(taskLabelUpdated({ id: \"638\", label: \"This is an event\" }));\n}\n\nfunction handleDishesButtonClicked() {\n  bus.publish(\n    taskCreated({ id: \"123\", listId: \"345\", value: \"Do the dishes\" })\n  );\n}\n```\n\n### Using a plain event object\n\nIf you want to avoid the direct dependency with your event creator you can use the plain event object:\n\n```tsx\nbus.publish({\n  type: \"kickoff.some.process\",\n  payload: props.data\n});\n```\n\n### Republishing events\n\nLets say you have received a remote event from a websocket and you need to prevent it from being automatically redispatched you can provide custom metadata with each publication of an event to prevent re-emmission of events over the socket.\n\n```ts\nimport p from \"pdsl\";\n\n// get an event from a socket\nsocket.on(\"event-sync\", (event: BusEvent\u003cany\u003e) =\u003e {\n  bus.publish(event, { remote: true });\n});\n\n// This is a shorthand utility that creates predicate functions to match based on a given object shape.\n// For more details see https://github.com/ryardley/pdsl\nconst isSharedAndNotRemoteFn = p`{\n  type: ${/^shared\\./},\n  meta: {\n    remote: !true\n  }\n}`;\n\n// Prevent sending a event-sync if the event was remote\nbus.subscribe(isSharedAndNotRemoteFn, event =\u003e {\n  socket.emit(\"event-sync\", event);\n});\n```\n\n### Switching on Events and Discriminated Unions\n\n```ts\n// This function creates foo events\nconst fooCreator = createEventDefinition\u003c{\n  foo: string;\n}\u003e()(\"shared.foo\");\n\n// This function creates bar events\nconst barCreator = createEventDefinition\u003c{\n  bar: string;\n}\u003e()(\"shared.bar\");\n\n// Create a union type to represent your app events\ntype AppEvent = ReturnType\u003ctypeof fooCreator\u003e | ReturnType\u003ctypeof barCreator\u003e;\n\nbus.subscribe(\"shared.**\", (event: AppEvent) =\u003e {\n  switch (event.type) {\n    case String(fooCreator):\n      // compiler is happy about payload having a foo property\n      alert(event.payload.foo.toLowerCase());\n      break;\n    case String(barCreator):\n      // compiler is happy about payload having a bar property\n      alert(event.payload.bar.toLowerCase());\n      break;\n    default:\n  }\n});\n```\n\n### Wildcard syntax\n\nYou can namespace your events using period delimeters. For example:\n\n```\n\"foo.*\" matches \"foo.bar\"\n\"foo.*.thing\" matches \"foo.fing.thing\"\n\"**\" matches everything eg \"foo\" or \"foo.bar.baz\"\n\"*\" matches everything within a single namespace eg. \"foo\" but not \"foo.bar\"\n```\n\nThis is inherited directly from EventEmitter2 which ts-bus currently uses under the hood.\n\n## React extensions\n\nIncluded with `ts-bus` are some React hooks and helpers that provide a bus context as well as facilitate state management within React.\n\n### BusProvider\n\nWrap your app using the `BusProvider`\n\n```tsx\nimport React from \"react\";\nimport App from \"./App\";\n\nimport { EventBus } from \"ts-bus\";\nimport { BusProvider } from \"ts-bus/react\";\n\n// global bus\nconst bus = new EventBus();\n\n// This wraps React Context and passes the bus to the `useBus` hook.\nexport default () =\u003e (\n  \u003cBusProvider value={bus}\u003e\n    \u003cApp /\u003e\n  \u003c/BusProvider\u003e\n);\n```\n\n### useBus\n\nAccess the bus instance with `useBus`\n\n```tsx\n// Dispatch from deep in your application somewhere...\nimport { useBus } from \"ts-bus/react\";\nimport { kickoffSomeProcess } from \"./my-events\";\n\nfunction ProcessButton(props) {\n  // Get the bus passed in from the top of the tree\n  const bus = useBus();\n\n  const handleClick = React.useCallback(() =\u003e {\n    // Fire the event\n    bus.publish(kickoffSomeProcess(props.data));\n  }, [bus]);\n\n  return \u003cButton onClick={handleClick}\u003eGo\u003c/Button\u003e;\n}\n```\n\n### useBusReducer\n\nThis connects state changes to bus events via a state reducer function.\n\nIts signature is similar to useReducer except that it returns the state object instead of an array:\n\nExample:\n\n```ts\nfunction init(initCount: number) {\n  return { count: initCount };\n}\n\n// dispatch is an alias to bus.publish() you can use either\nconst [state, dispatch] = useBusReducer(reducer, initCount, init);\n```\n\n```tsx\nimport { useBus, useBusReducer } from \"ts-bus/react\";\n\nconst initialState = { count: 0 };\n\nfunction reducer(state, event) {\n  switch (event.type) {\n    case \"counter.increment\":\n      return { count: state.count + 1 };\n    case \"counter.decrement\":\n      return { count: state.count - 1 };\n    default:\n      throw new Error();\n  }\n}\n\nfunction Counter() {\n  const bus = useBus();\n  const [state, dispatch] = useBusReducer(reducer, initialState);\n  return (\n    \u003c\u003e\n      Count: {state.count}\n      \u003cbutton onClick={() =\u003e bus.publish({ type: \"counter.increment\" })}\u003e\n        +\n      \u003c/button\u003e\n      \u003cbutton onClick={() =\u003e dispatch({ type: \"counter.decrement\" })}\u003e-\u003c/button\u003e\n    \u003c/\u003e\n  );\n}\n```\n\n### Custom subscriber function\n\nYou can configure `useBusReducer` with a custom `subscriber` passing in an options object.\n\n```ts\n// get a new useReducer function\nconst useReducer = useBusReducer.configure({\n  subscriber: (dispatch, bus) =\u003e {\n    bus.subscribe(\"count.**\", dispatch);\n  }\n});\n\nconst [state, dispatch] = useReducer(/*...*/);\n```\n\nNOTE: Boilerplate can be reduced by using the `reducerSubscriber` function.\n\n```ts\nuseBusReducer.configure({\n  subscriber: reducerSubscriber(\"count.**\")\n});\n```\n\n#### Usage with Redux dev tools\n\nYou can use ts-bus with Redux Devtools by using [Reinspect](https://github.com/troch/reinspect).\n\nHere is an example:\n\n```tsx\nimport React from \"react\";\nimport { StateInspector, useReducer as useReinspectReducer } from \"reinspect\";\nimport { EventBus, createEventDefinition } from \"ts-bus\";\nimport { BusProvider, useBus, useBusReducer } from \"ts-bus/react\";\n\nconst bus = new EventBus();\n\nexport default function AppWrapper() {\n  return (\n    \u003cBusProvider value={bus}\u003e\n      \u003cStateInspector name=\"App\"\u003e\n        \u003cApp /\u003e\n      \u003c/StateInspector\u003e\n    \u003c/BusProvider\u003e\n  );\n}\n\nconst useReducer =\n  process.env.NODE_ENV === \"development\" \u0026\u0026 window.__REDUX_DEVTOOLS_EXTENSION__\n    ? useBusReducer.configure({\n        useReducer: (reducer, initState, initializer) =\u003e\n          useReinspectReducer(reducer, initState, initializer, \"MyApp\") // passing in the reinspect id\n      })\n    : useBusReducer;\n\nconst increment = createEventDefinition()(\"increment\");\nconst decrement = createEventDefinition()(\"decrement\");\n\nfunction App() {\n  const b = useBus();\n  const [state] = useReducer(\n    (state, action) =\u003e {\n      switch (action.type) {\n        case `${increment}`: {\n          return {\n            ...state,\n            count: state.count + 1\n          };\n        }\n        case `${decrement}`: {\n          return {\n            ...state,\n            count: state.count - 1\n          };\n        }\n      }\n      return state;\n    },\n    { count: 0 }\n  );\n\n  return (\n    \u003cdiv\u003e\n      \u003cbutton onClick={() =\u003e b.publish(decrement())}\u003e-\u003c/button\u003e\n      {state.count} \u003cbutton onClick={() =\u003e b.publish(increment())}\u003e+\u003c/button\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n#### useBusReducer configuration\n\nAvailable options:\n\n| Option     | Description                               |\n| ---------- | ----------------------------------------- |\n| subscriber | Reducer subscriber definition             |\n| useReducer | Alternate React.useReducer implementation |\n\n### useBusState\n\nThis connects state changes to bus events via a useState equivalent function.\n\n```tsx\nimport { useBus, useBusState } from \"ts-bus/react\";\n\nconst setCountEvent = createEventDefinition\u003cnumber\u003e()(\"SET_COUNT\");\n\nfunction Counter() {\n  const bus = useBus();\n  const [count] = useBusState(0, setCountEvent);\n\n  return (\n    \u003c\u003e\n      Count: {count}\n      \u003cbutton onClick={() =\u003e bus.publish(setCountEvent(count + 1))}\u003e+\u003c/button\u003e\n      \u003cbutton onClick={() =\u003e bus.publish(setCountEvent(count - 1))}\u003e-\u003c/button\u003e\n    \u003c/\u003e\n  );\n}\n```\n\n#### Preconfigured useBusState\n\nYou can preconfigure useState to use a specific eventCreator and you get a drop in replacement for setState that is hooked up to the event bus.\n\nHere is a more complete example:\n\n```ts\n// events.ts\nexport const bus = new EventBus();\n\nexport const setCountEvent = createEventDefinition\u003cnumber\u003e()(\"SET_COUNT\");\n\nbus.subscribe(setCountEvent, event =\u003e {\n  console.log(`Setting count to ${event.payload}`);\n});\n```\n\n```tsx\n// App.ts\nimport { bus } from \"./events\";\n\nexport default function App() {\n  return (\n    \u003cBusProvider value={bus}\u003e\n      \u003cCounter /\u003e\n    \u003c/BusProvider\u003e\n  );\n}\n// ...\n```\n\n```tsx\n// Counter.ts\nimport { useBus, useBusState } from \"ts-bus/react\";\nimport { setCountEvent } from \"./events\";\n\nconst useState = useBusState(setCountEvent);\n\nfunction Counter() {\n  const bus = useBus();\n  const [count, setCount] = useState(0);\n\n  return (\n    \u003c\u003e\n      Count: {count}\n      \u003cbutton onClick={() =\u003e bus.publish(setCount(count + 1))}\u003e+\u003c/button\u003e\n      \u003cbutton onClick={() =\u003e bus.publish(setCount(count - 1))}\u003e-\u003c/button\u003e\n    \u003c/\u003e\n  );\n}\n```\n\n#### useBusState configuration\n\nYou can configure useBusState with a subscriber passing in an options object.\n\n```ts\n// get a new useState function\nconst useState = useBusState.configure(someEvent, {\n  subscriber: (dispatch, bus) =\u003e bus.subscribe(\"**\", ev =\u003e dispatch(ev.payload))\n});\n\nconst state = useState(/*...*/);\n```\n\nNOTE: The boilerplate code can be reduced by using the stateSubscriber function.\n\n```ts\nconst useState = useBusState.configure(someEvent, {\n  subscriber: stateSubscriber(\"**\")\n});\n```\n\nAvailable options:\n\n| Option     | Description                 |\n| ---------- | --------------------------- |\n| subscriber | State subscriber definition |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fryardley%2Fts-bus","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fryardley%2Fts-bus","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fryardley%2Fts-bus/lists"}