02 Event Bus - christoph-fricke/xsystem GitHub Wiki

The XSystem package started with the search of a proper event bus implementation that fits well into XState-based actor systems.

An event bus is a publisher that proxies received events to all interested subscribers. Therefore, in XSystem an event bus is just an actor that is spawned from the provided event-bus behavior, which allows for the greatest flexibility when integrating an event bus in an actor system.

Furthermore, nothing prevents the usage of multiple event busses in an application. In fact, XSystem encourages the use of multiple busses for different concerns, instead of using on ginormous bus. For example, one event bus for all events that are triggered by a user, and one event bus for all events that are received via a WebSocket connection.

EventBus

The EventBus interface defines the type for an event-bus actor reference. It is most useful to type function parameters for behaviors and machines that depend on an event bus.

import { spawnBehavior } from "xstate/lib/behaviors";
import { EventBus, createEventBus } from "xsystem";

type UsedEvents = { type: "hello" } | { type: "world" };
type MoreEvents = { type: "some" } | { type: "more" };

const eventBus = spawnBehavior(createEventBus<UsedEvents | MoreEvents>());

// Use the event bus somewhere
function createExampleMachine(bus: EventBus<UsedEvents>) {
  // Machine definition creation...
}

// Notice how a bus can be passed that supports more events than the required events.
createExampleMachine(eventBus);

createEventBus

The factory function createEventBus is used to create an event-bus behavior, which defines the behavior for a spawned event-bus actor.

createEventBus supports multiple strategies that define how events are proxied to subscribers.

  • direct (default): Only proxies to subscribers of that specific event bus.
  • global-broadcast: Uses a BroadcastChannel to distribute an event to all event-bus actors that are spawned with the same ID for an origin, e.g. synchronization across browser tabs.
  • broadcast: Similar to global-broadcast but limits the distribution to the same context, e.g. a single tab.

A broadcast strategy may be used to simplify the communication between actors spawned in different WebWorkers. In workers, a bus can be spawned with the same actor id, which will synchronize bus events between the WebWorkers.

In case the runtime does not support the BroadcastChannel API, it will fallback to the direct strategy with a no-operation polyfill for BroadcastChannel.

import { spawnBehavior } from "xstate/lib/behaviors";
import { createEventBus } from "xsystem";

type Events = { type: "hello" } | { type: "world" };

// Uses the direct communication strategy
const directBus = spawnBehavior(createEventBus<Events>());

// Uses the global-broadcast strategy with the same actor id.
// Subscribers to both actors will reive an event that is send to any of those actors.
const globalBroadcastBus1 = spawnBehavior(
  createEventBus<Events>({ strategy: "global-broadcast" }),
  { id: "gBus" }
);
const globalBroadcastBus2 = spawnBehavior(
  createEventBus<Events>({ strategy: "global-broadcast" }),
  { id: "gBus" }
);

// Same as above, but only broadcasts inside a its context (not between browser tabs).
const broadcastBus = spawnBehavior(
  createEventBus<Events>({ strategy: "broadcast" }),
  { id: "bus" }
);

Provide a custom polyfill for BroadcastChannel

createEventBus uses a fallback to a no-op channel when the native API is not present. To avoid this, a custom BroadcastChannel factory can be provided, which will be used to receive a BroadcastChannel instance when present. This is a great way to bring your own polyfill. If such factory is present in the event-bus options (see example), it will take precedence over the native API and no-op fallback. The id that is provided to the factory should be used as the channel name and equals the id that is given to the spawned actor.

import { spawnBehavior } from "xstate/lib/behaviors";
import { createEventBus } from "xsystem";
import { BroadcastChannel } from "some-bc-polyfill-package";

type Events = { type: "hello" } | { type: "world" };

const broadcastBus = spawnBehavior(
  createEventBus<Events>({
    strategy: "broadcast",
    channel: (id) => new BroadcastChannel(id),
  }),
  { id: "bus" }
);
⚠️ **GitHub.com Fallback** ⚠️