Effective state machines

Effect Days 2024

David Khourshid ยท @davidkpiano

stately.ai

for complex logic

  • Make API call to auth provider
  • Initiate OAuth flow
  • Ensure token is valid
  • Persist token as cookie
  • Redirect to logged in view

Given a user is logged out,

When the user logs in with correct credentials

Then the user should be logged in

function transition(state, event) {
  switch (state.value) {
    case 'cart':
      if (event.type === 'CHECKOUT') {
        return { value: 'shipping' };
      }
      return state;

    case 'shipping':
      // ...

    default:
      return state;
  }
}

State machines

with switch statements

State

Event

const machine = {
  initial: 'cart',
  states: {
    cart: {
      on: {
        CHECKOUT: 'shipping'
      }
    },
    shipping: {
      on: {
        NEXT: 'contact'
      }
    },
    contact: {
      // ...
    },
    // ...
  }
}

State machines

with object lookup

State

Event

State machines

with object lookup

function transition(state, event) {
  const nextState = machine
    .states[state]
    .on?.[event.type]
    ?? state;
}

transition('cart', { type: 'CHECKOUT' });
// => 'shipping'

transition('cart', { type: 'UNKNOWN' });
// => 'cart'
const machine = {
  initial: 'cart',
  states: {
    cart: {
      on: {
        CHECKOUT: 'shipping'
      }
    },
    shipping: {
      on: {
        NEXT: 'contact'
      }
    },
    contact: {
      // ...
    },
    // ...
  }
}
import { createMachine } from 'xstate';

const machine = createMachine({
  initial: 'cart',
  states: {
    cart: {
      on: {
        CHECKOUT: 'shipping'
      }
    },
    shipping: {
      on: {
        NEXT: 'contact'
      }
    },
    contact: {
      // ...
    },
    // ...
  }
});
import { setup } from 'xstate';

const machine = setup({
  types: {/* ... */},
  actions: {
    // Action implementations
  },
  actors: {
    // Actor implementations
  },
  guards: {
    // Guard implementations
  },
}).createMachine({
  initial: 'cart',
  states: {
    cart: {
      on: {
        CHECKOUT: 'shipping'
      }
    },
    shipping: {
      on: {
        NEXT: 'contact'
      }
    },
    contact: {
      // ...
    },
    // ...
  }
});
import { setup, createActor } from 'xstate';

const machine = setup({
  // ...
}).createMachine({
  // ...
});

const actor = createActor(machine);

actor.subscribe(snapshot => {
  console.log(snapshot.value);
});

actor.start();
// logs 'cart'

actor.send({ type: 'CHECKOUT' });
// logs 'shipping'

state.new

The actor model

๐Ÿค– All computation is performed within an actor

๐Ÿ“ฉ Actors can communicate only through messages

๐Ÿ’ฅ In response to a message, an actor can:

๐Ÿ“ฌ Send messages to other actors

๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Create a finite number of child actors

๐ŸŽญ Change its state/behavior

๐Ÿค– All computation is performed within an actor

๐Ÿ“ฉ Actors can communicate only through messages

๐Ÿ’ฅ In response to a message, an actor can:

๐Ÿ“ฌ Send messages to other actors

๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Create a finite number of child actors

๐ŸŽญ Change its state/behavior

The actor model

๐Ÿค– All computation is performed within an actor

๐Ÿ“ฉ Actors can communicate only through messages

๐Ÿ’ฅ In response to a message, an actor can:

๐Ÿ“ฌ Send messages to other actors

๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Create a finite number of child actors

๐ŸŽญ Change its state/behavior

The actor model

๐Ÿค– All computation is performed within an actor

๐Ÿ“ฉ Actors can communicate only through messages

๐Ÿ’ฅ In response to a message, an actor can:

๐Ÿ“ฌ Send messages to other actors

๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Create a finite number of child actors

๐ŸŽญ Change its state/behavior

The actor model

๐Ÿค– All computation is performed within an actor

๐Ÿ“ฉ Actors can communicate only through messages

๐Ÿ’ฅ In response to a message, an actor can:

๐Ÿ“ฌ Send messages to other actors

๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Create a finite number of child actors

๐ŸŽญ Change its state/behavior

The actor model

๐Ÿง‘โ€๐Ÿ’ป

๐Ÿง”

๐Ÿ‘ฉโ€๐Ÿณ

โ˜•๏ธโ”

๐Ÿ“

๐Ÿ’ถโ“

๐Ÿ’ถ

๐Ÿ“„โœ…

โ˜•๏ธ

โ˜•๏ธ

๐Ÿง‘โ€๐Ÿ’ป

๐Ÿง”

I would like a coffee...

What would you like?

ย Actorย 

Einen Kaffee bitte

ย Actorย 

๐Ÿ’ญ

๐Ÿ’ฌ

๐Ÿ’ฌ

Here you go, American โ˜•๏ธ

Dankeschรถn!

{
  orders: [/* ... */],
  inventory: {
    // ...
  },
  sales: 134.65
}

Actor logic

Actor System

โœ‰๏ธ

state 1

โœ‰๏ธ

state 2

โœ‰๏ธ

โœ‰๏ธ

processing...

state ??

Actor mailboxes

โœ‰๏ธ

โœ‰๏ธ

โœ‰๏ธ

โœ‰๏ธ

Actor mailboxes

โœ‰๏ธ

state 1

โœ‰๏ธ

โœ‰๏ธ

โœ‰๏ธ

Actor mailboxes

โœ‰๏ธ

state 1

โœ‰๏ธ

state 2

โœ‰๏ธ

โœ‰๏ธ

Actor mailboxes

โœ‰๏ธ

state 1

โœ‰๏ธ

state 2

โœ‰๏ธ

โœ‰๏ธ

state 3

โœ‰๏ธ

Actor mailboxes

โœ‰๏ธ

state 1

โœ‰๏ธ

state 2

โœ‰๏ธ

โœ‰๏ธ

state 3

โœ‰๏ธ

state 4

Actor mailboxes

import {
  fromPromise
} from 'xstate';

const promiseLogic = fromPromise(async ({ input }: {
  input: { userId: string }                                        
}) => {
  // TODO: use Effect
  const user = await getUser(input.userId);

  return user;
});

Promise logic

import {
  fromTransition
} from 'xstate';

const countLogic = fromTransition((state, event) => {
  switch (event.type) {
    case 'increment': {
      return { count: state.count + 1 };
    }
    case 'decrement': {
      return { count: state.count - 1 };
    }
    default: {
      return state;
    }
  }
}, { count: 0 }); // Initial state

Transition logic

import {
  fromCallback
} from 'xstate';

const resizeLogic = fromCallback(({ sendBack, receive }) => {
  const resizeHandler = (event) => {
    sendBack(event);
  };

  window.addEventListener('resize', resizeHandler);

  const removeListener = () => {
    window.removeEventListener('resize', resizeHandler);
  }

  receive(event => {
    if (event.type === 'stopListening') {
      console.log('Stopping listening');
      removeListener();
    }
  });

  // Cleanup function
  return () => {
    console.log('Cleaning up');
    removeListener();
  }
});

Callback logic

import {
  fromObservable
} from 'xstate';
import { interval } from 'rxjs';

const intervalLogic = fromObservable(
  ({ input }: { input: number }) => interval(input));

Observable logic

import {
  setup
} from 'xstate';

const machineLogic = setup({
  // ...
}).createMachine({
  initial: 'speaking',
  states: {
    speaking: {
      // ...
    },
    relaxing: {
      type: 'final'
    }
  }
});

State machine logic

import {
  createActor
} from 'xstate';

import { someLogic } from './someLogic';

const actor = createActor(someLogic, {
  input: {/* some input */}
});

actor.subscribe(snapshot => {
  console.log(snapshot.status, snapshot.output);
});

actor.start();

actor.send({ type: 'someEvent', data: 42 })

Actors

zio.dev/zio-actors/

@statelyai/inspect

import {
  createBrowserInspector
} from '@statelyai/inspect';

const inspector = createBrowserInspector();

inspector.actor('speaker');

inspector.actor('listener');

inspector.event('speaker', 'question?', {
  source: 'listener'
});

inspector.event('listener', 'answer!', {
  source: 'speaker'
});
export const machine = setup({
  types: {
    context: {} as {
      videoRef: VideoRef;
      videoUrl?: string;
    },
    input: {} as {
      videoRef: VideoRef;
    },
  },
  actions: {
    playVideo: (_, params: { videoRef: VideoRef }) =>
      Effect.sync(() => {
        params.videoRef.current?.play();
      }).pipe(Effect.runSync),
    pauseVideo: (_, params: { videoRef: VideoRef }) =>
      Effect.sync(() => {
        params.videoRef.current?.pause();
      }).pipe(Effect.runSync),
  },
  actors: {
    loadVideo: fromPromise(() => Effect.runPromise(program)),
    videoEnded: fromCallback<any, { videoRef: any }>(({ input, sendBack }) => {
      const videoRef = input.videoRef;
      const onEnded = () => {
        Effect.sync(() => {
          sendBack({ type: 'video.ended' });
        }).pipe(Effect.runSync);
      };
      videoRef.current?.addEventListener('ended', onEnded);
      return () => {
        videoRef.current?.removeEventListener('ended', onEnded);
      };
    }),
    keyEscape: fromCallback(({ input, sendBack }) => {
      const onKeydown = (event: KeyboardEvent) => {
        if (event.key === 'Escape') {
          Effect.sync(() => {
            sendBack({ type: 'key.escape' });
          }).pipe(Effect.runSync);
        }
      };
      window.addEventListener('keydown', onKeydown);
      return () => {
        window.removeEventListener('keydown', onKeydown);
      };
    }),
  }
}).createMachine({
  // ...
})
const machine = setup({
  // ...
}).createMachine({
  id: 'Video player machine',
  context: ({ input }) => ({
    videoRef: input.videoRef
  }),
  initial: 'loading',
  states: {
    loading: {
      invoke: {
        src: 'loadVideo',
        onDone: {
          target: 'mini',
        },
      },
    },
    mini: {
      id: 'mini',
      on: {
        toggle: {
          target: 'full',
        },
      },
    },
    full: {
      entry: {
        type: 'playVideo',
        params: ({ context }) => ({
          videoRef: context.videoRef,
        }),
      },
      exit: {
        type: 'pauseVideo',
        params: ({ context }) => ({
          videoRef: context.videoRef,
        }),
      },
      invoke: [
        {
          src: 'videoEnded',
          input: ({ context }) => ({ videoRef: context.videoRef }),
        },
        {
          src: 'keyEscape',
        },
      ],
      initial: 'playing',
      states: {
        playing: {
          on: {
            'video.ended': {
              target: 'stopped',
            },
          },
        },
        stopped: {
          after: {
            '2000': {
              target: '#mini',
            },
          },
        },
      },
      on: {
        toggle: {
          target: 'mini',
        },
        'key.escape': {
          target: 'mini',
        },
      },
    },
  },
});

sandromaglione.com/articles/getting-started-with-xstate-and-effect-audio-player

@SandroMaglione

State machine

State

Effect

Effect

State

Effect

Effect

Effect

Stateful thinking

Effectful thinking

Thank you!

Resources

Effect Days 2024

David Khourshid ยท @davidkpiano
stately.ai

Effective state machines for complex logic

By David Khourshid

Effective state machines for complex logic

  • 149