Pomodoro State Machine using XState
Tags: xstate, statemachine, programming, typescript
Published: 2022-02-12 15:57:58

State machines and XState are definitely a hot🔥 topic these days and beyond the many examples that one can find online, I wanted to try it out creating a finite state machine for a *"home made"* Pomodoro project.


![Pomodoro app gif](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d1v0685rw2138jelnhhe.gif)

## A bit about XState 
 
According to the documentation itself:

> XState is a library for creating, interpreting, and executing finite state machines and statecharts, as well as managing invocations of those machines as actors. The following fundamental computer science concepts are important to know how to make the best use of XState, and in general for all your current and future software projects.

This library has a rich and well maintained documentation, which I totally encourage to go over 🤓
[XState main page](https://xstate.js.org/docs/about/concepts.html)

## Pomodoro technique 🍅

This is popular method for time management, in which you alternate pomodoros (focused work sessions) with short and long breaks, promoting a sustained concentration and avoiding mental fatigue. 

![alt focus](https://c.tenor.com/rfqzP6ARmIEAAAAC/focus-center.gif)


## XState implementation

This machine has 4 states 

- `FOCUS` (initial state)
- `SHORT_BREAK`
- `LONG_BREAK`
- `DONE` (final state)

`FOCUS`, `SHORT_BREAK` and `LONG_BREAK` will also have inner states to control whether the current state is `RUNNING` or `PAUSED`. `RUNNING` is the initial state that we can be toggled through `PAUSE` and `CONTINUE` transitions.

Both states will transition back to `FOCUS` once `COMPLETED` is fired.


![Pomodoro State machine Visualizer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lkpjmsufkoc90orgyf4a.png)

<a href="https://stately.ai/viz/3f059dd7-a8d0-4054-98e7-906a7fa0fcc9" target="_blank">Link to visualizer</a>


We will be using context to control:
- focus (focus time in minutes)
- shortBreak (in minutes)
- longBreak (in minutes)
- intervalsForLongBreak 
- completedSections
- pausedTime (Date set when FOCUS, SHORT_BREAK or LONG_BREAK get paused)
- sectionTimeout (Timeout date for FOCUS, SHORT_BREAK or LONG_BREAK)     


But you might be asking yourself: "How will the machine control the sections and trigger the right transitions alone?"

Well, with XState we can use `Actions`, which are also commonly known as effects or side-effects. With these actions we will be controlling the sections.

The approach here was to invoke a service `timerInterval` on every `RUNNING` inner state. This interval will check always the time difference (seconds) between `sectionTimeout` and `now`. 

```typescript
services: {
  timerInterval: ctx => cb => {
    const interval = setInterval(() => {
      const now = new Date()
      const timeDiff = ctx.sectionTimeout ?
        Math.ceil((ctx.sectionTimeout.valueOf() - now.valueOf()) / 1000) :
        undefined

      if (timeDiff !== undefined && timeDiff <= 0) {
        cb('COMPLETED')
      }
    }, 1000)
    return () => clearInterval(interval)
  }
}
```

Since `sectionTimeout` is a specific date in the feature (`now` + focus time), it's getting updated on `FOCUS entry` through `setSectionTimeoutFromFocus`. We also need to handle cases when the user simply pauses the machine. 

To do so, we are managing `pausedTime` with `clearPausedTime` and `setPausedTime` and then using it's value to update `sectionTimeout` properly, meaning that every time that the user unpauses the machine `sectionTimeout` should be updated with `now + (sectionTimeout - pausedTime)`.

```typescript
[TimerStates.FOCUS]: {
  entry: ['setSectionTimeoutFromFocus', 'clearPausedTime'],
  initial: SectionStates.RUNNING,
  states: {
    [SectionStates.RUNNING]: {
      entry: 'setSectionTimeoutFromDiffPausedTime',
      invoke: {
        id: 'timerInterval',
        src: 'timerInterval'
      },
      on: {
        PAUSE: SectionStates.PAUSED
      }
    },
    [SectionStates.PAUSED]: {
      entry: 'setPausedTime',
      on: {
        CONTINUE: SectionStates.RUNNING
      }
    }
  }
}
```

```typescript
actions: {
  setSectionTimeoutFromFocus: assign({
    sectionTimeout: ctx => {
      const newSectionTimeout = new Date()
      newSectionTimeout.setMinutes(newSectionTimeout.getMinutes() + ctx.focus)

      return newSectionTimeout
    }
  }),
  setSectionTimeoutFromDiffPausedTime: assign({
    sectionTimeout: ctx => {
      if (!ctx.sectionTimeout || !ctx.pausedTime) {
        return ctx.sectionTimeout
      }

      const sectionTimeLeft = Math.ceil(
        ctx.sectionTimeout.valueOf() - ctx.pausedTime.valueOf()
      )

      const newSectionTimeout = new Date()
      newSectionTimeout.setMilliseconds(newSectionTimeout.getMilliseconds() + sectionTimeLeft)

      return newSectionTimeout
    }
  })
}
```
 
We are adding `sectionsCompleted` and `focusCompletedGoToLongBreak` guarded transitions to decide the next state transition, once `COMPLETED` is triggered (`timerInterval` service). 
`completedSections` gets also incremented on `FOCUS exit`.

```typescript
exit: 'increaseCompletedSections',
on: {
    COMPLETED: [
      {
        target: TimerStates.DONE,
        cond: 'sectionsCompleted'
      },
      {
        target: TimerStates.LONG_BREAK,
        cond: 'focusCompletedGoToLongBreak'
      },
      {
        target: TimerStates.SHORT_BREAK
      }
    ]
}
```
```typescript
guards: {
  focusCompletedGoToLongBreak: ctx =>
    (ctx.completedSections + 1) % ctx.intervalsForLongBreak === 0,
  sectionsCompleted: ctx => ctx.sections === ctx.completedSections + 1
}
``` 

`SHORT_BREAK` and `LONG_BREAK` states have the same structure as `FOCUS`, with the exception that their `COMPLETED` transitions lead back to `FOCUS`.

```typescript
[TimerStates.SHORT_BREAK]: {
  entry: ['setSectionTimeoutFromShortBreak', 'clearPausedTime'],
  initial: SectionStates.RUNNING,
  states: {
    [SectionStates.RUNNING]: {
      entry: 'setSectionTimeoutFromDiffPausedTime',
      invoke: {
        id: 'timerInterval',
        src: 'timerInterval'
      },
      on: {
        PAUSE: SectionStates.PAUSED
      }
    },
    [SectionStates.PAUSED]: {
      entry: 'setPausedTime',
      on: {
        CONTINUE: SectionStates.RUNNING
      }
    }
  },
  on: {
    COMPLETED: TimerStates.FOCUS
  }
},
[TimerStates.LONG_BREAK]: {
  entry: ['setSectionTimeoutFromLongBreak', 'clearPausedTime'],
  initial: SectionStates.RUNNING,
  states: {
    [SectionStates.RUNNING]: {
      entry: 'setSectionTimeoutFromDiffPausedTime',
      invoke: {
        id: 'timerInterval',
        src: 'timerInterval'
      },
      on: {
        PAUSE: SectionStates.PAUSED
      }
    },
    [SectionStates.PAUSED]: {
      entry: 'setPausedTime',
      on: {
        CONTINUE: SectionStates.RUNNING
      }
    }
  },
  on: {
    COMPLETED: TimerStates.FOCUS
  }
}
```

## Wrapping up

The entire implementation is available on GitHub ([timeTrackerMachine.ts](https://github.com/andrecrimb/pomodoro_rn/blob/master/src/machines/timeTrackerMachine.ts))

{% github https://github.com/andrecrimb/pomodoro_rn no-readme %}

I hope you enjoyed this article, if yes then don't forget to press 💚

See you next time 👋🏽