resFactory / factory

Resource Factory is a universal approach to originating, refining, and rendering Markdown, HTML, type-safe SQL, or other assets that could comprise static sites or engineering artifacts.
GNU Affero General Public License v3.0
0 stars 3 forks source link

Migrate SocketCommandsManager custom states to XState charts #74

Closed shah closed 2 years ago

shah commented 2 years ago

Our current SocketCommandsManager Javascript (browser client) user agent has a custom state manager. It would be better to switch to proper XState charts for stability and comprehension.

Start with: XState Websocket Machine.

websocketMachine.js:

import { createMachine, assign, send } from "xstate";

export const websocketMachine = createMachine(
  {
    id: "websocket",
    initial: "disconnected",
    context: {
      retries: 0,
      retryInterval: 1000,
      retryTick: 1,
      heartbeatTimer: 0,
      heartbeatInterval: 10000,
      timeUntilNextRetry: 1000
    },
    states: {
      connected: {
        entry: [
          "resetHeartbeatTimer",
          "resetRetryInterval",
          "setTimeUntilNextRetryToRetryInterval"
        ],
        invoke: {
          id: "heartbeatInterval",
          src: () => (cb) => {
            const interval = setInterval(() => {
              cb("HEARTBEAT_TICK");
            }, 1000);

            return () => {
              clearInterval(interval);
            };
          }
        },
        always: {
          target: "disconnected",
          cond: (context) =>
            context.heartbeatTimer >= context.heartbeatInterval,
          actions: "resetHeartbeatTimer"
        },
        on: {
          HEARTBEAT_TICK: {
            actions: "incrementHeartbeatTimer"
          },
          HEARTBEAT_MESSAGE: {
            actions: "resetHeartbeatTimer"
          }
        }
      },
      disconnected: {
        initial: "waiting",
        states: {
          retrying: {
            entry: [
              "doubleRetryInterval",
              "setTimeUntilNextRetryToRetryInterval"
            ]
          },
          waiting: {
            invoke: {
              id: "retryCountdown",
              src: (context) => (cb) => {
                const interval = setInterval(() => {
                  cb("RETRY_TICK");
                }, 1000 * context.retryTick);

                return () => {
                  clearInterval(interval);
                };
              }
            },
            on: {
              RETRY_TICK: {
                actions: "tickTimeUntilNextRetry"
              },
              RETRY: {
                actions: "retryNow"
              }
            },
            always: {
              target: "retrying",
              cond: (context) => context.timeUntilNextRetry <= 0
            }
          }
        }
      }
    },
    on: {
      CONNECT: "connected",
      DISCONNECT: "disconnected"
    }
  },
  {
    actions: {
      sendConnect: send("CONNECT"),
      doubleRetryInterval: assign({
        retryInterval: (context) =>
          Math.min(context.retryInterval * 2, 64000) +
          Math.floor(Math.random() * 1000)
      }),
      resetRetryInterval: assign({
        retryInterval: 1000
      }),
      resetHeartbeatTimer: assign({
        heartbeatTimer: 0
      }),
      incrementHeartbeatTimer: assign({
        heartbeatTimer: (context) => context.heartbeatTimer + 1000
      }),
      setTimeUntilNextRetryToRetryInterval: assign({
        timeUntilNextRetry: (context) => context.retryInterval
      }),
      tickTimeUntilNextRetry: assign({
        timeUntilNextRetry: (context) =>
          context.timeUntilNextRetry - context.retryTick * 1000
      }),
      retryNow: assign({
        timeUntilNextRetry: 0
      })
    }
  }
);

useMachine.js:

import { readable } from "svelte/store";
import { interpret } from "xstate";

export function useMachine(machine, options) {
  const service = interpret(machine, options);

  const store = readable(service.initialState, (set) => {
    service.onTransition((state) => {
      set(state);
    });

    service.start();

    return () => {
      service.stop();
    };
  });

  return {
    state: store,
    send: service.send
  };
}
shah commented 2 years ago

Use trebuchet-client to help maintain state instead of our own client code.