• Stars
    star
    134
  • Rank 270,967 (Top 6 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created over 5 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A lightweight JavaScript/TypeScript event bus to help manage your application architecture.

ts-bus

A lightweight TypeScript event bus to help manage your application architecture

Build Status codecov GitHub license

Example

import { EventBus, createEventDefinition } from "ts-bus";

// Define Event
export const someEvent = createEventDefinition<{ url: string }>()("SOME_EVENT");

// Create bus
const bus = new EventBus();

// Subscribe
bus.subscribe(someEvent, event => {
  alert(event.payload.url);
});

// Publish
bus.publish(someEvent({ url: "https://github.com" }));

Rationale

We 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.

This is the kind of thing that you could use effectively in most applications.

For my purposes I wanted a system that:

  • Is framework agnostic can support Vue, React or Angular.
  • Could enable micro-frontends / microlithic architecture.
  • Can easily use React hooks to reduce state in the case of React.
  • Does not conflate eventing with state management.
  • Has really good TypeScript support.

Alternatives

  • 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"
  • RxJS - could make a great event bus but feels too heavy handed for use with many projects.
  • 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.

Upgrading to v3

Version 3 includes a couple of breaking changes to the react extensions. Now both useBusState and useBusReducer return tuples the same as their React equivalents.

// Old 
const state = useBusReducer(/* ... */);

// New 
const [state, dispatch] = useBusReducer(/* ... */);
// Old
const count = useBusState(0, eventCreator);

// New
const useState = useBusState.configure(eventCreator);

const [count, setCount] = useState(0);

Also the configuration for useBusState has changed.

See useBusReducer, useBusState.

Installation

Use your favourite npm client to install ts-bus. Types are included automatically.

Npm:

npm install ts-bus

Yarn:

yarn add ts-bus

Example applications

With Redux Devtools.

Usage

Create a bus

Create your EventBus globally somewhere:

// bus.ts
import { EventBus } from "ts-bus";
export const bus = new EventBus();

Declare events

Next create some Events:

// events.ts
import { createEventDefinition } from "ts-bus";

export const taskCreated = createEventDefinition<{
  id: string;
  listId: string;
  value: string;
}>()("task.created");

export const taskLabelUpdated = createEventDefinition<{
  id: string;
  label: string;
}>()("task.label.updated");

Notice 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. See switching on events.

Runtime payload checking

You 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:

import p from "pdsl";

// pdsl creates predicate functions
const isLabel = p`{
  id: string,
  label: string,
}`;

export const taskLabelUpdated = createEventDefinition(isLabel)(
  "task.label.updated"
);

taskLabelUpdated({ id: "abc" }); // {"id":"abc"} does not match expected payload.

These warnings are suppressed in production.

Subscribing

import { taskLabelUpdated, taskCreated } from "./event";
import { bus } from "./bus";

// You can subscribe using the event creator function
bus.subscribe(taskLabelUpdated, event => {
  const { id, label } = event.payload; // Event is typed
  doSomethingWithLabelAndId({ id, label });
});

Unsubscribing

To unsubscribe from an event use the returned unsubscribe function.

const unsubscribe = bus.subscribe(taskLabelUpdated, event => {
  // ...
});

unsubscribe(); // removes event subscription

Subscribing with a type string

You can use the event type to subscribe.

bus.subscribe("task.created", event => {
  // ...
});

Or you can use wildcards:

bus.subscribe("task.**", event => {
  // ...
});

Subscribing with a predicate function

You can also subscribe using a predicate function to filter events.

// A predicate
function isSpecialEvent(event) {
  return event.payload && event.payload.special;
}

bus.subscribe(isSpecialEvent, event => {
  // ...
});

You may find pdsl a good fit for creating predicates.

Subscription syntax

As you can see above you can subscribe to events by using the subscribe method of the bus.

const unsubscriber = bus.subscribe(<string|eventCreator|predicate>, handler);

This subscription function can accept a few different options for the first argument:

  • A string that is the specific event type or a wildcard selector eg. mything.**.
  • An eventCreator function returned from createEventDefinition<PayloadType>()("myEvent")
  • 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'}

The returned unsubscribe() method will unsubscribe the specific event from the bus.

Publishing events

Now let's publish our events somewhere

// publisher.ts
import { taskLabelUpdated, taskCreated } from "./events";
import { bus } from "./bus";

function handleUpdateButtonClicked() {
  bus.publish(taskLabelUpdated({ id: "638", label: "This is an event" }));
}

function handleDishesButtonClicked() {
  bus.publish(
    taskCreated({ id: "123", listId: "345", value: "Do the dishes" })
  );
}

Using a plain event object

If you want to avoid the direct dependency with your event creator you can use the plain event object:

bus.publish({
  type: "kickoff.some.process",
  payload: props.data
});

Republishing events

Lets 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.

import p from "pdsl";

// get an event from a socket
socket.on("event-sync", (event: BusEvent<any>) => {
  bus.publish(event, { remote: true });
});

// This is a shorthand utility that creates predicate functions to match based on a given object shape.
// For more details see https://github.com/ryardley/pdsl
const isSharedAndNotRemoteFn = p`{
  type: ${/^shared\./},
  meta: {
    remote: !true
  }
}`;

// Prevent sending a event-sync if the event was remote
bus.subscribe(isSharedAndNotRemoteFn, event => {
  socket.emit("event-sync", event);
});

Switching on Events and Discriminated Unions

// This function creates foo events
const fooCreator = createEventDefinition<{
  foo: string;
}>()("shared.foo");

// This function creates bar events
const barCreator = createEventDefinition<{
  bar: string;
}>()("shared.bar");

// Create a union type to represent your app events
type AppEvent = ReturnType<typeof fooCreator> | ReturnType<typeof barCreator>;

bus.subscribe("shared.**", (event: AppEvent) => {
  switch (event.type) {
    case String(fooCreator):
      // compiler is happy about payload having a foo property
      alert(event.payload.foo.toLowerCase());
      break;
    case String(barCreator):
      // compiler is happy about payload having a bar property
      alert(event.payload.bar.toLowerCase());
      break;
    default:
  }
});

Wildcard syntax

You can namespace your events using period delimeters. For example:

"foo.*" matches "foo.bar"
"foo.*.thing" matches "foo.fing.thing"
"**" matches everything eg "foo" or "foo.bar.baz"
"*" matches everything within a single namespace eg. "foo" but not "foo.bar"

This is inherited directly from EventEmitter2 which ts-bus currently uses under the hood.

React extensions

Included with ts-bus are some React hooks and helpers that provide a bus context as well as facilitate state management within React.

BusProvider

Wrap your app using the BusProvider

import React from "react";
import App from "./App";

import { EventBus } from "ts-bus";
import { BusProvider } from "ts-bus/react";

// global bus
const bus = new EventBus();

// This wraps React Context and passes the bus to the `useBus` hook.
export default () => (
  <BusProvider value={bus}>
    <App />
  </BusProvider>
);

useBus

Access the bus instance with useBus

// Dispatch from deep in your application somewhere...
import { useBus } from "ts-bus/react";
import { kickoffSomeProcess } from "./my-events";

function ProcessButton(props) {
  // Get the bus passed in from the top of the tree
  const bus = useBus();

  const handleClick = React.useCallback(() => {
    // Fire the event
    bus.publish(kickoffSomeProcess(props.data));
  }, [bus]);

  return <Button onClick={handleClick}>Go</Button>;
}

useBusReducer

This connects state changes to bus events via a state reducer function.

Its signature is similar to useReducer except that it returns the state object instead of an array:

Example:

function init(initCount: number) {
  return { count: initCount };
}

// dispatch is an alias to bus.publish() you can use either
const [state, dispatch] = useBusReducer(reducer, initCount, init);
import { useBus, useBusReducer } from "ts-bus/react";

const initialState = { count: 0 };

function reducer(state, event) {
  switch (event.type) {
    case "counter.increment":
      return { count: state.count + 1 };
    case "counter.decrement":
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
  const bus = useBus();
  const [state, dispatch] = useBusReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => bus.publish({ type: "counter.increment" })}>
        +
      </button>
      <button onClick={() => dispatch({ type: "counter.decrement" })}>-</button>
    </>
  );
}

Custom subscriber function

You can configure useBusReducer with a custom subscriber passing in an options object.

// get a new useReducer function
const useReducer = useBusReducer.configure({
  subscriber: (dispatch, bus) => {
    bus.subscribe("count.**", dispatch);
  }
});

const [state, dispatch] = useReducer(/*...*/);

NOTE: Boilerplate can be reduced by using the reducerSubscriber function.

useBusReducer.configure({
  subscriber: reducerSubscriber("count.**")
});

Usage with Redux dev tools

You can use ts-bus with Redux Devtools by using Reinspect.

Here is an example:

import React from "react";
import { StateInspector, useReducer as useReinspectReducer } from "reinspect";
import { EventBus, createEventDefinition } from "ts-bus";
import { BusProvider, useBus, useBusReducer } from "ts-bus/react";

const bus = new EventBus();

export default function AppWrapper() {
  return (
    <BusProvider value={bus}>
      <StateInspector name="App">
        <App />
      </StateInspector>
    </BusProvider>
  );
}

const useReducer =
  process.env.NODE_ENV === "development" && window.__REDUX_DEVTOOLS_EXTENSION__
    ? useBusReducer.configure({
        useReducer: (reducer, initState, initializer) =>
          useReinspectReducer(reducer, initState, initializer, "MyApp") // passing in the reinspect id
      })
    : useBusReducer;

const increment = createEventDefinition()("increment");
const decrement = createEventDefinition()("decrement");

function App() {
  const b = useBus();
  const [state] = useReducer(
    (state, action) => {
      switch (action.type) {
        case `${increment}`: {
          return {
            ...state,
            count: state.count + 1
          };
        }
        case `${decrement}`: {
          return {
            ...state,
            count: state.count - 1
          };
        }
      }
      return state;
    },
    { count: 0 }
  );

  return (
    <div>
      <button onClick={() => b.publish(decrement())}>-</button>
      {state.count} <button onClick={() => b.publish(increment())}>+</button>
    </div>
  );
}

useBusReducer configuration

Available options:

Option Description
subscriber Reducer subscriber definition
useReducer Alternate React.useReducer implementation

useBusState

This connects state changes to bus events via a useState equivalent function.

import { useBus, useBusState } from "ts-bus/react";

const setCountEvent = createEventDefinition<number>()("SET_COUNT");

function Counter() {
  const bus = useBus();
  const [count] = useBusState(0, setCountEvent);

  return (
    <>
      Count: {count}
      <button onClick={() => bus.publish(setCountEvent(count + 1))}>+</button>
      <button onClick={() => bus.publish(setCountEvent(count - 1))}>-</button>
    </>
  );
}

Preconfigured useBusState

You 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.

Here is a more complete example:

// events.ts
export const bus = new EventBus();

export const setCountEvent = createEventDefinition<number>()("SET_COUNT");

bus.subscribe(setCountEvent, event => {
  console.log(`Setting count to ${event.payload}`);
});
// App.ts
import { bus } from "./events";

export default function App() {
  return (
    <BusProvider value={bus}>
      <Counter />
    </BusProvider>
  );
}
// ...
// Counter.ts
import { useBus, useBusState } from "ts-bus/react";
import { setCountEvent } from "./events";

const useState = useBusState(setCountEvent);

function Counter() {
  const bus = useBus();
  const [count, setCount] = useState(0);

  return (
    <>
      Count: {count}
      <button onClick={() => bus.publish(setCount(count + 1))}>+</button>
      <button onClick={() => bus.publish(setCount(count - 1))}>-</button>
    </>
  );
}

useBusState configuration

You can configure useBusState with a subscriber passing in an options object.

// get a new useState function
const useState = useBusState.configure(someEvent, {
  subscriber: (dispatch, bus) => bus.subscribe("**", ev => dispatch(ev.payload))
});

const state = useState(/*...*/);

NOTE: The boilerplate code can be reduced by using the stateSubscriber function.

const useState = useBusState.configure(someEvent, {
  subscriber: stateSubscriber("**")
});

Available options:

Option Description
subscriber State subscriber definition

More Repositories

1

rxjs-as-redux

A simple proof of concept Redux style framework only using Rx
JavaScript
77
star
2

pdsl

The expressive declarative toolkit for composing predicates in TypeScript or JavaScript
TypeScript
69
star
3

indexr

Autogenerate dynamic ES6 index modules.
JavaScript
44
star
4

react-native-c-plus-plus

Companion repository to the medium article https://medium.com/p/fb30b46c2468
Shell
25
star
5

reduxor

A React Router ready, Redux & React Isomorphic starting template for mortals.
JavaScript
24
star
6

jsx-switch

Simple switching for React JSX
JavaScript
8
star
7

use-reducer-enhancer

Enhance React Hooks with Redux middleware ✨
TypeScript
6
star
8

rxmsg

Universal messaging wrapper
TypeScript
5
star
9

nector

Functional transport layer for isomorphic apps
JavaScript
3
star
10

example-codesplitting-nested-routing

TypeScript
2
star
11

ryardley

Readme
2
star
12

microlithic-example

Example implementation of a microlithic app
TypeScript
2
star
13

zx-bug

Demonstrate how zx fails to pass on colors and what to do about it
JavaScript
2
star
14

ssr-next-hooks-example

An example of how to have SSR enabled next hooks
TypeScript
2
star
15

versionable

Memory efficient version control for your javascript objects
TypeScript
2
star
16

hello-webpack

A Webpack Hello World for easily testing things
JavaScript
2
star
17

viennajs-2019

JavaScript
2
star
18

succession

Tool for creating dynamic function chains.
JavaScript
1
star
19

babel-plugin-pdsl

This plugin speeds up `pdsl` in babelified codebases by pre-compiling p-expressions to predicate function definitions.
JavaScript
1
star
20

gulp-indexr

Gulp plugin for indexr. This is a stub.
1
star
21

jest-test-sandbox

debugging an issue with jest
TypeScript
1
star
22

berlinjs-2019

Slides for my lightening talk at Berlin JS
JavaScript
1
star
23

drator

A simple API for sending data from the server to the client in the context of an isomorphic app.
JavaScript
1
star
24

melbjs-201904

Presentation for MelbJS
JavaScript
1
star
25

rustlebrot

Mandelbrot set in Rust, WASM and JS
JavaScript
1
star
26

rpctest

TypeScript
1
star
27

prisma-introspect-error-example

TypeScript
1
star
28

isomorphic-next-hooks

Example of how to do isomorphic next hooks
JavaScript
1
star
29

console-timer

A better timer for the console.
JavaScript
1
star
30

url-modals

TypeScript
1
star