Coding the System Model, Part 2

Lincoln Anderson
Lincoln Anderson
May 3, 2022

In the previous post, I built out our models and relationships, and added some simple tasks. Now it’s time to add some calculations (ie. derived values).

Before I dig into each one, I want to ask: when does each calculation occur? Because I’m building an app where time is a factor, this matters quite a bit. Why?

Take one of our main calculations: “has the timer completed?” I can evaluate this when the timer loads, but I also need to re-evaluate it every moment that time changes! There are different techniques for doing this, but I’ve found the most success with a combination of requestAnimationFrame and setTimeout. This gives you a way to run some code no faster than the screen can update, and also caps it. (Think of this like a top-speed.) Some web browsers will let your app consume a ton of the user’s processor capacity, so I want to put a constraint on that.

Other calculations, like “is the timer currently paused”, only change when the user pauses or unpauses it. So these are just a consequence of what’s in the central data store. It does not need to be re-evaluated every moment.

So I have these two buckets of calculations. One is derived directly from the data, and then another is derived from that, plus the current time. I’ve chosen to call these two buckets TimerState and LiveTimerState. I’ve implemented both of these as React contexts so the calculations only need to be done once and can be consumed anywhere in scope.

TimerState Calculations

  • Is the Timer currently paused, or is it running?
  • I can answer this question by looking at the latest “start” or “stop” event. If it’s a “stop” event, the timer is paused. If it’s a “start” event, it’s running.

LiveTimerState Calculations

  • Has the Timer completed?
  • ~I can answer this question by adding up all the “running time” between start and stop events, and seeing if it exceeds the total duration of all segments.
  • What is the Timer's currently active interval?
  • ~Similarly to the above, I can get the total running time and compare it to the individual segment durations in order. When I find a segment that has started but has not completed, that’s the active one.

Note: We can observe that the above two calculations are quite similar, so one algorithm can actually solve both problems. If a timer has started and has no active segment, the timer must have completed!

Processes

Some of our processes are simply shortcuts to relevant tasks:

  • Start the Timer
  • Create a TimerEvent on the Timer with a type of “start”

Other processes are made up of a chain of tasks and subprocesses.

  • Start a Pomodoro Timer
  • ~Create a new Timer
  • ~Create preset segments on the Timer
  • ~Start the Timer
  • ~Watch a Timer's progress

These are all relatively easy to set up by creating new functions that call other functions, building up complexity as we go.

Still other processes are net-new:

  • Be alerted when a Timer Interval completes
  • ~???

These processes often relate to the user experience quite heavily, but we can still express them in relation to the system we’ve built so far.

Timer Alert process

Essentially I want to do “something”, when the LiveTimerState goes from “not completed” to “completed”. For our prototype, the “something” can simply occur on screen.

In React, we can use hooks to observe a value and do something when it changes. I used a combination of the usePrevious hook from react-use to track the value, and React’s native useEffect hook to perform the comparison. When the value changes, I play a sound effect using the Howl library. I packaged this functionality up in a hook called usePlaySoundOnChange and bound it to the LiveTimerState’s “completed” value. A similar approach could be used to animate confetti at the right time, or perform some other effect.

/* usePlaySoundOnChange.ts */

export const usePlaySoundOnChange = (path: string, value: any) => {
  const prevValue = usePrevious(value);

  React.useEffect(() => {
    if (prevValue && value !== prevValue) {
      var sound = new Howl({
        src: [path],
      });
      sound.play();
    }
  }, [value, prevValue, path]);
};

Summary

The TimerState and LiveTimerState can now handle anything we’d want to do with a timer. Any user interface in the system just needs to import one (or both) and begin calling methods. Here’s a summary of what you can get from each:

TimerState

  • timerId: (string)
  • timer: Raw Timer data.
  • loading: (boolean)
  • errorMessage: (string)
  • started: Whether or not the timer has started (boolean)
  • startOrResume: A function that will start the timer, or resume if paused.
  • stop: A function that will stop (or pause) the timer.
  • stoppedAt: The timestamp of the last stopped time, if there is one.
  • running: Whether or not the timer is ticking (boolean)
  • segments: The Timer’s segments, sorted by date (array)
  • totalDuration: (number, in milliseconds)
  • effectivelyStartedAt: A time that represents the actual start time plus the amount of time stopped. (It’s useful for quickly determining the time remaining.)

LiveTimerState

  • segments: The timer segments, but with extra values for “elapsed” and “remaining time” in milliseconds.
  • currentSegment: The currently active segment, if there is one.
  • finished: (boolean)
  • remainingTime: (number, in milliseconds)

Next steps

Now that our calculations and processes are in place, we actually have a functioning timer, it just happens to be extremely ugly! Now comes the fun part where I can take all the utilities I’ve created around our system model and experiment by binding them to different user experiences.

« previous postnext post »