JamieMason/self-help

View on GitHub
src/machine/tree/index.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import { assign, interpret, Interpreter, Machine, State, StateMachine } from 'xstate';
import { Branch, Node } from '../..';
import { actions } from './actions';
import { guards } from './guards';
import { services } from './services';

export interface TreeSchema {
  states: {
    visitNode: {};
    resolveBranch: {
      states: {
        failure: {};
        loading: {};
        success: {};
      };
    };
    renderBranch: {};
    renderLeaf: {};
    renderValue: {};
  };
}

export interface TreeContext<NodeType = Node> {
  currentNode: NodeType;
  error: Error | null;
  rootNode: Node;
}

export interface FinaliseEvent {
  type: 'FINALISE';
}

export interface RetryEvent {
  type: 'RETRY';
}

export interface SelectChildEvent {
  type: 'SELECT_CHILD';
  childIndex: number;
}

export interface SelectRootEvent {
  type: 'SELECT_ROOT';
}

export interface SetChildrenEvent {
  type: 'done.invoke.getChildren';
  data: Branch;
}

export type TreeEvent =
  | FinaliseEvent
  | RetryEvent
  | SelectChildEvent
  | SelectRootEvent
  | SetChildrenEvent;

export type TreeInterpreter = Interpreter<TreeContext, TreeSchema, TreeEvent>;
export type TreeState = State<TreeContext<Node>, TreeEvent>;
export type TreeMachine = StateMachine<TreeContext<Node>, TreeSchema, TreeEvent>;

export const createTreeMachine = (rootNode: Node, id: string = 'tree-machine'): TreeMachine => {
  const RENDER_BRANCH = `#${id}.renderBranch`;
  const RENDER_LEAF = `#${id}.renderLeaf`;
  const RENDER_VALUE = `#${id}.renderValue`;
  const RESOLVE_BRANCH = `#${id}.resolveBranch`;
  const VISIT_NODE = `#${id}.visitNode`;
  return Machine<TreeContext, TreeSchema, TreeEvent>(
    {
      context: {
        currentNode: rootNode,
        error: null,
        rootNode,
      },
      id,
      initial: 'visitNode',
      states: {
        visitNode: {
          on: {
            '': [
              { cond: 'isAsyncBranch', target: RESOLVE_BRANCH },
              { cond: 'isBranch', target: RENDER_BRANCH },
              { cond: 'isLeaf', target: RENDER_LEAF },
            ],
          },
        },
        resolveBranch: {
          initial: 'loading',
          states: {
            loading: {
              invoke: {
                id: 'getChildren',
                src: 'getChildren',
                onDone: {
                  target: 'success',
                  actions: assign((context, event) => ({ currentNode: event.data })),
                },
                onError: {
                  target: 'failure',
                  actions: assign((context, event) => ({ error: event.data })),
                },
              },
            },
            success: {
              on: {
                '': {
                  target: VISIT_NODE,
                },
              },
            },
            failure: {
              on: {
                RETRY: {
                  target: 'loading',
                },
              },
            },
          },
        },
        renderBranch: {
          on: {
            SELECT_CHILD: {
              actions: ['selectChild'],
              target: VISIT_NODE,
            },
            SELECT_ROOT: {
              actions: ['selectRoot'],
              target: VISIT_NODE,
            },
          },
        },
        renderLeaf: {
          on: {
            FINALISE: RENDER_VALUE,
            SELECT_ROOT: {
              actions: ['selectRoot'],
              target: VISIT_NODE,
            },
          },
        },
        renderValue: {
          type: 'final',
        },
      },
    },
    {
      actions,
      guards,
      services,
    },
  );
};

export const createTreeInterpreter = (rootNode: Node, id?: string): TreeInterpreter =>
  interpret(createTreeMachine(rootNode, id));