Neverending Pile of Messages

    oktopod
    TypeScript icon, indicating that this package has built-in type declarations

    1.2.6 • Public • Published

    Oktopod

    Event Bus for Xstate Machines

    GitHub Workflow Status Codecov GitHub license

    Small (~1KB) event bus implementation that is primarily made for sending events between Xstate services that are completely decoupled, and it also supports regular listeners (functions).

    Play with a simple demo on codesandbox

    Motivation

    After working with Xstate on a couple of projects, one problem was always present. At some point, there was always a need to enable communication between services that are completely decoupled. There was a need for some kind of event bus. So I've made this little module that enables just that.

    This module is a specialized event bus that can route events between services.

    Installation

    npm install oktopod

    Communication between services

    There are two ways for the event bus to work with Xstate services.

    1. By registering the service with the event bus. When the service is registered, other services can send events directly to the registered service, just by knowing the service id.

    2. Register the service with the event bus for specific events. This way the service can respond to any event that is emitted via the event bus.

    Registering the service with the event bus

    Let's start with creating the machine, starting the service, and registering the service with the event bus.

    import Oktopod from 'oktopod'
    import { interpret } from 'xstate'
    import { createMachine } from './machines'
    // create the event bus
    const eventBus = new Oktopod()
    
    const service = interpret(createMachine()).start()
    
    const unregisterFn = eventBus.register(service)
    //unreigster later
    unregisterFn()
    // or
    eventBus.unreigster(service)

    When the service is registered with the event bus, it can be retrieved with the service id

    eventBus.getServiceById(service.id)

    Other services can send events directly to the registered service, all they need is access to the event bus.

    In the next example, the service (machineTwo) has access to the event bus, and when the service receives the SEND_HELLO event, it will send the HELLO event with data {foo:'bar'} to the service with the id of machineOne (which is registered with the event bus)

    export function machineTwo(eventBus: Oktopod) {
      // event bus has special actions for xstate services
      const { sendTo } = eventBus.actions
    
      return {
        id: 'machineTwo',
        initial: 'idle',
        states: {
          idle: {
            on: {
              SEND_HELLO: {
                actions: sendTo('machineOne', {
                  type: 'HELLO',
                  data: { foo: 'bar' }
                })
              }
            }
          }
        }
      }
    }
    
    const machineOne = {
      id: 'machineOne',
      initial: 'idle',
      states: {
        idle: {
          on: {
            HELLO: {
              //^  triggered via event bus
              actions: (_ctx, evt) => {
                //evt: {type: 'HELLO', data:{foo:'bar'}}
              }
            }
          }
        }
      }
    }
    
    const serviceOne = interpret(machineOne).start()
    //serviceTwo needs access to the even bus
    const serviceTwo = interpret(machineTwo(eventBus)).start()
    
    //serviceOne needs to be registered with the event bus
    eventBus.register(serviceOne)
    
    serviceTwo.send({ type: 'SEND_HELLO' })

    Another way to send the even to the machine is by using the event bus directly.

    eventBus.register(serviceOne)
    
    /*
    if useStrict is true, the event bus will throw if
    service is not present or does not accept the event
     */
    const useStrict = true
    eventBus.sendTo(
      service.id,
      {
        type: 'EVENT_A',
        data
      },
      useStrict
    )

    Few very important things to keep in mind:

    • serviceOne will only receive the HELLO event if the machine is in the state where one of the next accepted events is HELLO. This protects from services that have strict mode enabled (which throws an error if the service can't receive the event that is sent to it)

    • serviceOne will only receive the event if it running. If the service is in a final state then the event will not be sent.

    Event bus action helpers

    As seen in the previous example event bus comes with some xstate actions that can be used from inside the machine. You can access them via actions property on the Oktopod instance

    eventBusInstance.actions //property

    actions property is bound to the event bus instance so it can safely be de-structured

    const { sendTo, forwardTo } = eventBusInstance.actions

    sendTo

    sendTo Sends an event to Xstate service that is registered with the event bus.

    sendTo('machineOne', {
      // or array of id's ['machineOne','machineX']
      type: 'HELLO',
      data: { foo: 'bar' }
    })
    
    // OR
    
    sendTo(
      (ctx, evt) => {
        return 'moonMachine'
        // or
        return ['machineOne', 'machineX', 'machineY']
      },
      (ctx, evt) => {
        return {
          type: 'HELLO',
          data: { earthTemp: '46℃' }
        }
      }
    )
    // OR
    // combination of the above
    
    //strict mode
    sendTo('machineOne', {}, true)

    In strict mode (third argument), the event bus will an throw error if the service that is being sent to is not registered with the event bus.

    forwardTo

    forwardTo Forwards the received event to a service that is registered with the event bus. In the next example, machineOne will forward SOME_EVENT to machineTwo (which is registered with the event bus).

    const { forwardTo } = eventBusInstance.actions
    
    createMachine({
      id: 'machineOne',
      initial: 'idle',
      context: {},
      states: {
        idle: {
          on: {
            SOME_EVENT: {
              actions: [forwardTo('machineTwo')]
            }
          }
        }
      }
    })

    You can use it like this:

    forwardTo('machineOne')
    
    // or
    forwardTo(['machineOne', 'machineTwo', 'machineThree'])
    // or
    forwardTo((ctx, evt) => {
      return 'machineOne'
      // or
      return ['machineOne', 'machineTwo', 'machineThree']
    })
    //strict mode
    forwardTo('machineOne', true)

    In strict mode, the event bus will throw an error if the service that is being forwarded to is not registered with the event bus.

    emit

    emit Emits the event on the event bus and any listeners that are registered for that specific event will be triggered.

    emit('HELLO_WORLD', { foo: 'bar' })
    // OR
    emit(
      (ctx, evt) => {
        return 'HELLO_WORLD'
      },
      (ctx: any) => {
        return { foo: 'bar' }
      }
    )

    Register Service for event bus events

    In addition to facilitating communication between services, event bus can directly register xstate service to respond to events that are sent via event bus (you can also register for all events that are emitted from the even bus by using a "*" as event name)

    const eventBus = new Oktopod()
    
    const unsubscribeFn = bus.on('HELLO_WORLD', service, 'EVENT_ON_SERVICE')
    //or register for all events
    const unsubscribeFn = bus.on('*', service, 'EVENT_ON_SERVICE')
    
    //emit the event
    eventBus.emit('HELLO_WORLD', { foo: 'bar' })
    
    //example machine
    const machine = {
      initial: 'idle',
      context: {},
      states: {
        idle: {
          on: {
            EVENT_ON_SERVICE: {
              actions: (ctx, evt) => {
                // evt will be:
                //   {
                //   type: 'EVENT_ON_SERVICE', <- xstate event
                //   event: 'HELLO_WORLD', <- bus event
                //   data: { foo: 'bar' } <- bus event data
                // }
              }
            }
          }
        }
      }
    }
    
    //unregister later
    unsubscribeFn()
    // or
    eventBus.off('HELLO_WORLD', service)

    In the example above, machine service has been registered for the HELLO_WORLD event on the event bus, and when that event is emitted, the service will receive EVENT_ON_SERVICE event.

    Like in the service to service communication previously documented, the service will not receive the event if it is not supported, or the service is not running (it's in its final state, and doesn't accept any events)

    Simple function listeners

    Note: This part has nothing to do with xstate services

    Event bus can also accept functions as listeners. This functionality enables you to hook other parts of your app to the event bus.

    import Oktopod, { EventPayload } from 'oktopod'
    
    const eventBus = new Oktopod()
    
    //register simple function
    const listener = (evt: EventPayload<{ foo: string }>) => {
      //  evt will be:
      //{ data: { foo: 'bar' }, event: 'hello' }
    }
    
    const unregister = eventBus.on('hello', listener)
    //unregister later
    unregister()
    // or
    eventBus.off('hello', listener)
    
    //trigger the listener
    eventBus.emit('hello', { foo: 'bar' })

    Unregister all listeners

    You can unregister all listeners for a specific event. This will clear all listeners (services and functions).

    eventBus.clear('event_name')

    API docs

    Oktopod is written in TypeScript, auto generated API documentation is available.

    License

    This project is licensed under the MIT License - see the LICENSE file for details


    Oktopod means Octopus in Serbian :)

    Install

    npm i oktopod

    DownloadsWeekly Downloads

    2

    Version

    1.2.6

    License

    MIT

    Unpacked Size

    126 kB

    Total Files

    27

    Last publish

    Collaborators

    • ivandotv