Skip to content

jtmckay/mobus

Repository files navigation

Mo'Bus

Supercharge your state management with events, predictable state machines, and extensible streams.

Adapt any existing class, MobX store, or Javascript Object to be event driven. Great for 3D projects in ThreeJS.

Installation

npm i mobus

Why?

You are looking for:

  • pure function state machines, to avoid the pain from side effects
  • event driven architecture, streaming solutions, or functional reactive programming
  • how to leverage the power of RxJS (EGs in React with MobX)

Example

https://github.com/jtmckay/mobus/tree/main/examples/preact

Control state with commands

import { definedEntity, stateMachineFactory } from 'mobus';

export const counterStore = { count: 0 }

export const { commandFactory, subscribe } = stateMachineFactory('counter', {
  storeSingle: counterStore
});
subscribe();

export const increment = commandFactory<void>({
  eventHandler: (entity) => {
    const counter = definedEntity(entity)
    counter.count++;
    return counter
  }
})

If you need it in a render cycle (EG: React with MobX)

import { definedEntity, stateMachineFactory } from 'mobus';
import { observable, runInAction } from 'mobx';

export const counterStore = observable({ count: 0 }) // Turn object into MobX observable

export const { commandFactory, subscribe } = stateMachineFactory('counter', {
  wrapper: runInAction, // MobX wrapper function
  storeSingle: counterStore
});
subscribe();

export const increment = commandFactory<void>({
  eventHandler: (entity) => {
    const counter = definedEntity(entity)
    counter.count++;
    return counter
  }
})

Then render it in React, Preact etc.

import { increment, counterStore } from '../../domain/counter/counter.bus';
import { observer } from 'mobx-react-lite';

const Counter = observer(() => (
  <div onClick={() => increment()}>
    Counter: {counterStore.count}
	</div>
));

Optimistic updates

This example uses a more advanced store. EG: Map<string, Pedometer>.

import { stateMachineFactory, WithID } from 'mobus';
import { pedometerStore } from './pedometer.store';

export const { commandFactory, subscribe } = stateMachineFactory(ENTITY, { wrapper: runInAction, store: pedometerStore });
subscribe();

const syncHeartRate = commandFactory<WithID & { rate: number }>({
  eventHandler: (entity, event) => {
    // EG: optional eventHandler immediately updates the state
    const pedometer = definedEntity(entity);
    pedometer.heartRate = event.payload.rate;
    return pedometer;
  },
  asyncEventHandler: async (entity, event) => {
    // EG: optional asyncEventHandler will update the state again (after hitting server etc.)
    const pedometer = definedEntity(entity);
    await new Promise((resolve) => setTimeout(resolve, 2000));
    runInAction(() => {
      pedometer.heartRate = 100;
    });
    return pedometer;
  },
});

Parallel handlers

By default, all stores will only handle one event at a time, and will queue any events that are triggered in the meantime.

import { stateMachineFactory } from 'mobus';
import { pedometerStore } from './pedometer.store';

export const { commandFactory, subscribe } = stateMachineFactory(ENTITY, {
  wrapper: runInAction,
  store: pedometerStore,
  parallel: true, // Setting parallel to true will allow multiple async handlers to fire simultaneously
});

Testing

Incredibly simple testing when compared to most RxJS implementations, because it exposes a promise that can be awaited.

describe('when incrementing the counter with a delay', () => {
  beforeEach(async () => {
    await delayedIncrement()
  });

  it('increases the count to 1', () => {
    expect(counterStore.count).toBe(1)
  });
});

Future examples

With events driving the system, it is trivial to develop advanced features for your product such as:

  • stream interactions over websockets for live collaboration
  • analytics for user engagement or business changelog

Flow

graph TD
    subgraph RxJS Events
        Event1["Command 1"]
        Event2["Command 2"]
        Event3["Command 3"]
        Event4["Command 4"]
    end
    
    subgraph Bus
        Event4 --> Commands
        Event3 --> Commands
        Event2 --> Commands
        Event1 --> Commands
        Commands --> Get
        Database -->Get
        Get --> Function["Pure Function Bussiness Logic Handlers"]
        Function --> Set
        Set --> Database["MobX Store"]
    end

    Database --> React["React (any UI)"]

    Function --> EntityStream["RxJS Stream"]
Loading

Developing & Publishing

yarn build

cd package

npm login --auth-type=legacy

npm publish

About

Turn state into an RxJS state machine

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published