Getting started /w Livenote 🔥

REST API

Please check our API v1 or API v2 documentation. We will supply you with a permanent bearer token to consume the API.

Example usage Pusher client

In our client side application React we have a WebsocketBuddy service which we use to connect with Pusher. The following examples contain redacted and/or modified code for you to get an idea of how to get started.

Environment variables

We're using environment variables in a dotenv file, .env:

PUSHER_APP_KEY=pusher-key
PUSHER_APP_CLUSTER=pusher-cluster

We will supply the correct credentials.

Code

For the intents of the purposes of this example, the file is located at services/websockets/index.js.

import Pusher from 'pusher-js'

const PusherInstance = (channelId, listeners) => {
  // Initiate Pusher (see: https://pusher.com/docs/channels/using_channels/client-api-overview/)
  const pusher = new Pusher(process.env.PUSHER_APP_KEY, {
      cluster: process.env.PUSHER_APP_CLUSTER,
  })

  // Subscribe to Pusher channel (see: https://pusher.com/docs/channels/using_channels/public-channels/)
  const channel = pusher.subscribe(channelId)

  // Bind to channel events
  Object.keys(listeners).forEach((listener) => {
    const callback = listeners[listener]

    if ('function' !== typeof callback) {
      throw new Error (`Listener callback must be a function, but got ${typeof callback}`)
    }

    // Bind (see https://pusher.com/docs/channels/using_channels/events/#binding-on-the-channel)
    channel.bind(listener, callback)
  })

  return { pusher, channel }
}

export const WebsocketBuddy = ({
  channel = null,
  driver = null,
  listeners = {},
}) => {
  if (!channel) {
    throw new Error(`Channel is required`)
  }

  if (!driver) {
    throw new Error(`Driver is required`)
  }

  switch (driver) {
    case 'Pusher':
      return PusherInstance(channel, listeners)
    default:
      throw new Error(`Driver ${driver} not supported`)
  }
}

Subscribing to Pusher channel

We're using a container/component pattern with Redux-Saga, because our application is quite large – and growing. You probably won't need to follow our code examples to a tee, but for good measure we'll include everything so you can pick and choose how to go about using Pusher.

Form container

import Form from 'pages/Form'
import { connect } from 'react-redux'
import {
  REQUEST_GET_GAME,
  REQUEST_GET_GAME_CLOCK,
  REQUEST_GET_GAME_EVENTS,
  REQUEST_GET_GAME_SCORE,
} from 'redux/actions/game'

const mapStateToProps = (state, { id, sportSlug }) => ({
  id,
  sportSlug,
  clock: state.game.clock,
  game: state.game.form,
  events: state.game.events,
  score: state.game.score,
})

const mapDispatchToProps = (dispatch,  { id, sportSlug }) => ({
  get: () => {
    // API call: https://data.livenote.nl/api/v1/documentation#/Game/game.get
    dispatch({ type: REQUEST_GET_GAME, id })
    // API call: https://data.livenote.nl/api/v1/documentation#/Game/clock.get
    dispatch({ type: REQUEST_GET_GAME_CLOCK, id })
    // API call: https://data.livenote.nl/api/v1/documentation#/Game/events.get
    dispatch({ type: REQUEST_GET_GAME_EVENTS, id })
    // API call: https://data.livenote.nl/api/v1/documentation#/Game/score.get
    dispatch({ type: REQUEST_GET_GAME_SCORE, id })
  },
})

export default connect(mapStateToProps, mapDispatchToProps)(Form)

Form component

Then, in our form we import the WebsocketBuddy service and initiate it as follows:

import React, { useEffect } from 'react'
import { WebsocketBuddy } from 'services/websockets'

const Form = ({
  id,
  clock,
  events,
  game,
  get,
  score,
  sportSlug,
}) => {
  useEffect(() => {
    // On page (re)load, get data from API
    get()
     
    // Subscribe to Pusher for realtime updates
    const { channel, pusher } = WebsocketBuddy({
      driver: 'Pusher',
      channel: `${sportSlug}-${id}`,
      listeners: { 
        clockUpdated: ({ message }) => {
          console.log(message) // See docs below for description of object
        },
      }
    });

    return function cleanUp() {
      // Unbind all handlers
      channel.unbind()

      // Disconnect from Pusher
      pusher.disconnect()
    }
  }, [])

  // Do cool stuff with clock, events, game and score props

  return (
    <div>Cool form component 😎</div>
  )
}

export default Form

Pusher messages

With the coding out of the way, lets quickly run over the different types of messages your application can expect

clockUpdated event

property description type values
type type of event string start_first_quarter, end_first_quarter, start_second_quarter, end_second_quarter, start_third_quarter, end_third_quarter, start_fourth_quarter, end_fourth_quarter, pause or resume
direction whether clock counts up or down. string down or up
trigger whether to start or stop the clock string start or stop
delta which time (in seconds) to set on the clock int

clockUpdated message example

{
  type: "start_first_quarter",
  direction: "down",
  trigger: "start",
  delta: 900
}

Setting clock delta

With the exception of pause and resume event types, all events signify a fixed point in the game which can be used as a (re)calibration point for the clock and should be considered safe.

The pause and resume events occur during game play. If for some reason the graphics overlay application is out of synch, updating the clock time from the delta property on pause and resume type events, could have some weird side effects. It's probably best to simply either start or stop the clock without updating the clock time when these events occur.

matchUpdated event

property description type values
type type of event string bully
timestamp pure game time of event in seconds int

matchUpdated message example

{
  type: "bully",
  timestamp: 850
}

scoreUpdated event

property description type values
type type of event string goal, goal_penalty_corner, goal_penalty_stroke, goal_shoothout
team_id scoring team uuid string
player_id scoring player (if available) uuid string
score update score for team int

scoreUpdated message example

{
  type: "goal",
  team_id: "8cfab860-2aef-4f62-b503-abb8ed3f11fa",
  player_id: "ab38d444-ff55-41ab-8f4b-015d08c33293",
  score: 1
}

timelineUpdated event

property description type values
type type of event string yellow_card, green_card, long_corner, foul, red_card, penalty_corner, penalty_stroke, substitution_in, substitution_out, free_hit
team_id team uuid string
player_id player (if available) uuid string
timestamp pure game time of event in seconds int

timelineUpdated message example

{
  type: "foul",
  team_id: "8cfab860-2aef-4f62-b503-abb8ed3f11fa",
  player_id: "ab38d444-ff55-41ab-8f4b-015d08c33293",
  timestamp: 850
}