React/Redux + Play with Websockets

I’ve been spending more and more time working with React and Redux.  One particular project I’m working on will require a number of different timers to be running simultaneously.  The timers will be set to various durations and have a few requirements:

  • They must be defined and executed on the backend
  • They must send individual timer ticks to the client for each second elapsed
  • They must be pause-able and resume-able without losing resolution

I decided this was a good exercise to explore in the blog as it illustrates a number of the aspects of developing a one page application using React + Redux with a Scala back end.

Server

The heart of this example is the TimerPlugin, an Akka Actor which manages all the timer functionality.  The plugin responds to a number of messages.  Using Akka messaging, you can :

  • Set a timer for a duration
  • Pause an actively running timer
  • Resume a paused timer
  • Receive regular TimerTicks from the running timer

Below are the case classes that make up the messaging API for the plugin:


case class SetTimer (duration: Long, sendTicks: Boolean, actor: ActorRef)
case class TimerTick(uuid: String, remaining: Long, elapsed: Long, isPaused: Boolean) 

case class Timer(
  actor : ActorRef,
  running: Boolean = false,
  pauseTime: Long = 0,
  pauseStartTime : Long = 0,
  isPaused: Boolean = false,
  endTime : Long = 0,
  startTime : Long = 0,
  isSet : Boolean = false,
  sendTicks : Boolean = false,
  lastTick : Long = 0
)
case class Poll()
case class PauseTimer(uuid: String)
case class ResumeTimer(uuid: String)
case class TimerAlarm(uuid: String, elapsed: Long)

When the TimerPlugin receives a SetTimer message, it creates the appropriate timer and returns a unique identifier (UUID) to the caller. Subsequent calls to TimerPlugin require the UUID to be present in the message, and messages sent by the timer (e.g. TimerTicks) also contain the UUID.

The receive loop for the TimerPlugin is straight forward:


 def receive = {

    case (a : SetTimer) => {
      val now = new java.util.Date().getTime
      val uuid = java.util.UUID.randomUUID.toString
      timers(uuid) =
        Timer(
          isSet = true,
          startTime = now,
          endTime = now + a.duration,
          running = true,
          actor = a.actor,
          sendTicks = a.sendTicks
        )
      sender ! uuid
    }

    case (PauseTimer(uuid)) => {
      if (timers.contains(uuid)) {
        val a = timers(uuid)
        if (!a.isPaused && a.running) {
          timers(uuid) = a.copy(isPaused = true, pauseStartTime = new Date().getTime)
          timers(uuid).actor ! TimerTick(uuid, calcRemaining(timers(uuid)), calcElapsed(timers(uuid)), true)
        }
      }
    }

    case (ResumeTimer(uuid)) => {
      if (timers.contains(uuid)) {
        val a = timers(uuid)
        if (a.isPaused & a.running) {
          val paused_time = a.pauseTime + new Date().getTime() - a.pauseStartTime
          timers(uuid) = a.copy(isPaused = false, pauseTime = paused_time)
        }
      }
    }

    case (Poll) => {
      checkTimers()
    }

    case msg => {
      Logger.warn("Unhandled message: " + msg)
    }

  }

There are four possible messages:

  • SetTimer – The plugin sets up a new timer, stores it in the timers map, then generates and returns the UUID.
  • PauseTimer – Pauses the timer (if it exists)
  • ResumeTimer – Resumes the timer
  • Poll – This message is sent after 500ms and is the event driver for the entire plugin.  This is not a ideal solution, but for this particular illustration, is adequate.

Since this article is more about wiring up technologies, I’m not going to discuss in detail the actual algorithm that manages the timer internals.  The code for that is in the checkTimers() function, and is left as an exercise for the reader to explore if desired.

The WebSocket is implemented as an additional Akka actor and doesn’t really do anything special; mostly acting as a proxy between Akka and the world.

class MyWebSocketActor(out: ActorRef, timer: ActorRef) extends Actor {

  // Respond to both TimerPlugin and WebSocket origin messages
  def receive = {

    case (tick : TimerTick) => {
      out ! ("{ \"event\": \"timer-tick\", \"id\": \""+tick.uuid+"\", \"remaining\": \""+tick.remaining+"\" , \"isPaused\": \""+tick.isPaused+"\" }")
    }

    case (alarm : TimerAlarm) => {
      out ! ("{ \"event\": \"timer-alarm\", \"id\": \""+alarm.uuid+"\", \"elapsed\": \""+alarm.elapsed+"\" }")
    }

    case msg: String => {
      val json = Json.parse(msg)
      (json \ "action").as[String] match {
        case "set-timer" => {
          val duration = (json \ "value").as[String]
          implicit val timeout = Timeout(5 seconds)
          val future = timer ? SetTimer(duration.toLong, true, self)
          val uuid = Await.result(future, timeout.duration).asInstanceOf[String]
        }
        case "pause-timer" => {
          val uuid = (json \ "value").as[String]
          timer ! PauseTimer(uuid)
        }
        case "resume-timer" => {
          val uuid = (json \ "value").as[String]
          timer ! ResumeTimer(uuid)
        }
        case _ =>  { Logger.warn("Unhandled message") }
      }
    }
  }
}

The WebSocket actor will forward both TimerTicks and TimerAlarms to the listening client without further processing.  And it will process action message requests from the client to set, pause and resume a timer.

Client

There is one reducer to handle all the state changes for the app, and there is one entry for state: an array of timers.  Newly created timers are added into the array, and action types of timer-tick and timer-alarm update the appropriate timer entry.

const myReducer = function(state = fromJS(
	{
		timers: []
	}), action) {

	var timers = state.get('timers')

	if (action.type === 'timer-tick') {
		if (!timers.find(x => x.get('id') === action.payload.id)) {
			var newlist = timers.push(fromJS(
				{
					id : action.payload.id,
					remaining : action.payload.remaining,
					isPaused : action.payload.isPaused,
					isRunning : "true"
				}))
			return state.set('timers', newlist)
		}
		else {
			var index = timers.findIndex(x => x.get('id') === action.payload.id)
			var newlist = timers.set(index, fromJS(
				{
					id : action.payload.id,
					remaining : action.payload.remaining,
					isPaused : action.payload.isPaused,
					isRunning : "true"
				}))
			return state.set('timers', newlist)
		}
	}
	// Handle the timer alarm when it arrives
	else if (action.type === 'timer-alarm') {
		var index = timers.findIndex(x => x.get('id') === action.payload.id)
		var entry = timers.get(index)
		var newlist = timers.set(index, fromJS(
			{
				id : entry.get('id'),
				remaining : entry.get('remaining'),
				isPaused : entry.get('isPaused'),
				isRunning : "false"
			}))
		return state.set('timers', newlist)
	}
	return state
}

The WebSocket interface for the client is simple.  New messages are dispatched through the reducer and a wssend method is exported for sending messages to the back-end.

ws.onmessage = function (e) {
	let result = JSON.parse(e.data);
	store.dispatch({type: result.event, payload: result})
}

export function wssend(message) {
	var json_message = JSON.stringify(message)
	ws.send(json_message)
}

All that remains is a web-page, connected to the Redux store that will respond appropriately to the given state changes, which ended up looking like this:

 

I’m use React-Bootstrap and am pretty pleased so far with how it works. A single local React state variable is used to track the timer duration in question, and callbacks are used to fire requests using the WebSocket connection. The submit callback for the form is below. It does some simple validation, converts the seconds to milliseconds, and fires the request over the socket:

	handleSubmit(e) {
		e.preventDefault()
		if (this.getValidationState() != "success") return
		var val = parseInt(this.state.value) * 1000
		wssend({ action: "set-timer", value: val.toString() })
		this.setState({ value: ""  })
		console.log("handle submit")
	}

How it all works:

  • A user defines a timer and clicks Submit
  • handleSubmit fires a request over the WebSocket to the back-end to create a timer
  • The back-end create and returns a reference to the timer over the WebSocket
  • The front-end receives the reference and updates its state accordingly, the web-page is then aware of the timer and can display it
  • The back-end sends event information about the timer (ticks, alarms, etc.) over the WebSocket that the client uses to update its state

The source code for both server and client can be checked out, tested and tweaked here:

You should be able to get these talking to each other quickly by following the README(s).

-mwa