Time as State

Post by Saul Shanabrook

Recently, I have been working on a stage lighting control system built with web technologies.

In the front end, I have been enjoying Cerebral to work as my controller. I recommend you refer to it's website for an introduction to how it works, if you are unfamiliar with it.

On my lighting console, the user defines multiples "cues" and transitions between them. Usually this will be a linear fade over some set amount of time. So the user presses a button and the the cue fade begins, replacing the current cue with the next.

I have implemented all the fading logic on the server, so the client just has to say "begin fade" and then the server will take over and actually do the hard work of fading over time. However, it makes sense to display, in the browser, the fade over time, both so that the user can see how far it has progressed and so that they can take manual control at any time to stop the fade at any point.

Here is the working example at the end:

I am

Solution

I choose to achieve this effect by putting the current time inside the state. Then, the component that renders that input can get the current time, and the start time of the transition, and compute how far along it should be.

This requires four different parts.

1) a path in the state to record the current time.
2) an action that updates that path to the actual current time
3) a signal that calls that actions
4) a component to emit that signal continuously

Following my previous post on organizing Cerebral and React code, I created a subfolder called time, with these four parts in it.

$ tree app/time/                                                                                                                                                                                                                app/time/
├── actions.js
├── components
│   └── Now.js
├── signals.js
├── state.js
└── utils.js

1 directory, 5 files  

state.js

I chose my path as ['local', 'time'].

Then, for the initial state I just created a string version of the current time.

import moment from 'moment'

import {stringify} from './utils'

export default {  
  local: {
    now: stringify(moment())
  }
}

As you can see I am using Moment JS for date/time encoding and decoding. You probably could just use the native Date module, but I am using Moment anyway later, so I just used it because it was easiest. The stringify function just turns the moment object into a string, by using the format method.

actions.js

Then the action just has to update this path

import moment from 'moment'

import {stringify} from './utils'

export function updateNow (input, state) {  
  state.set(['local', 'now'], stringify(moment()))
}

signals.js

The signal just calls this action

import * as a from './actions'

export default {  
  timePassed: [a.updateNow]
}

components/Now.js

This component just calls the signal every 30ms until is destroyed. It seemed a bit weird to me at first to have a component that did not exist at all in the UI, but I ended up choosing the way instead of like putting the signal calling in my app initialization or something. Since it is a component, if you don't need to update the current time at any point, then you can just remove this component. For example, I just stick this next to my updating cue progress bar, so that whenever that isn't displayed in the UI, this isn't triggered either.

import {Component} from 'react'  
import {Decorator as Cerebral} from 'cerebral-react';

const REFRESH_INTERVAL = 30 // in milliseconds

export default @Cerebral({})  
class Now extends Component {

  stop = false

  refresh () {
    this.props.signals.timePassed()

    if (!this.stop) {
      this.refreshInFuture()
    }
  }

  refreshInFuture () {
    setTimeout(this.refresh.bind(this), REFRESH_INTERVAL)
  }

  componentDidMount () {
    this.refreshInFuture()
  }

  componentWillUnmount () {
    this.stop = true
  }

  render () {
    return null
  }
}

Further questions

While this does work, the cerebral debugger has trouble keeping up with high frequency (< 1 / 50ms) signal creation.

Also, if you render the Now component twice, with this implementation it will send the signal twice as much, which is not ideal. If I end up doing this, I will have to look into some sort of singleton component in React or something.