Complete Introduction To Real-time

Long Polling

In this course, we are going to learn about Polling, Http2 push, Web sockets, and Socket io. When we talk about real-time, we mean that we have some sort of server state and some sort of client state and we want them to be the same. Whether that is a chat client, which is what we are going to be doing today. Sending messages back and forth and if I send a message, I want everyone to see it. So that would be me sending a message to the server and the server broadcasting that. To follow along, you should understand Javascript and Node JS.

POLLING

HTTP Polling is a mechanism where the client keeps requesting the resource at regular intervals. If the resource is available, the server sends the resource as part of the response. If the resource is not available, the server returns an empty response back to the client. The backend setup is no different than a normal express server.

FOLDER STRUCTURE

To follow along, git clone https://github.com/amschel99/polling.git

cd polling

npm install

npm start

Your chat client will be displayed on port 3000 Screenshot (321).png

Screenshot (319).png

BACKEND OVERVIEW

The Backend is no different than a typical express server

import express from "express";
import bodyParser from "body-parser";
import nanobuffer from "nanobuffer";
import morgan from "morgan";

// set up a limited array


const msg = new nanobuffer(50);
const getMsgs = () => Array.from(msg).reverse();

// feel free to take out, this just seeds the server with at least one message
msg.push({
  user: "brian",
  text: "hi",
  time: Date.now(),
});

// get express ready to run
const app = express();
app.use(morgan("dev"));
app.use(bodyParser.json());
app.use(express.static("frontend"));

app.get("/poll", function (req, res) {
res.json({msg:getMsgs()})
});

app.post("/poll", function (req, res) {
const{user, text}=req.body
msg.push(
  {
user,
  text,
  time: Date.now()
  }

)
res.json({
  status:'OK'
})
});

// start the server
const port = process.env.PORT || 3000;
app.listen(port);
console.log(`listening on http://localhost:${port}`);

We use nanobuffer package to set a limited array.

getMsgs() returns the messages in the array in reverse order(returns the latest messages to be pushed)

The /poll endpoint sends all the messages in the msg array to the client when the client makes a GET request.

The /poll endpoint pushes the req.body object to the msg array when the client makes a POST request.

THE CLIENT

const chat = document.getElementById("chat");
const msgs = document.getElementById("msgs");

// let's store all current messages here
let allChat = [];

// the interval to poll at in milliseconds
const INTERVAL = 3000;

// a submit listener on the form in the HTML
chat.addEventListener("submit", function (e) {
  e.preventDefault();
  postNewMsg(chat.elements.user.value, chat.elements.text.value);
  chat.elements.text.value = "";
});

async function postNewMsg(user, text) {
const data={user,text}
const options={
  method:'POST',
  body:JSON.stringify(data),
  headers:{
    'Content-type':'application/json',

  }
}
await  fetch('/poll',options)

}

async function getNewMsgs() {
  let json;
  try{
const res= await fetch('/poll')
json= await res.json()
if(res.status>=400){
  throw new Error(`request did not succeed ${res.status}`)
}
allChat=json.msg;
  render()
  failedTries=0
  }
  catch(e){
    console.error(` polling error ${e}`)
failedTries++
  }


}

function render() {
  // as long as allChat is holding all current messages, this will render them
  // into the ui. yes, it's inefficent. yes, it's fine for this example
  const html = allChat.map(({ user, text, time, id }) =>
    template(user, text, time, id)
  );
  msgs.innerHTML = html.join("\n");
}

// given a user and a msg, it returns an HTML string to render to the UI
const template = (user, msg) =>
  `<li class="collection-item"><span class="badge">${user}</span>${msg}</li>`;

// make the first request
const BACKOFF=5000
let failedTries=0
let timeToMakeNextRequest=0
async function rafTimer(time){
  if(timeToMakeNextRequest<=time){
await getNewMsgs();
  timeToMakeNextRequest=time +INTERVAL + failedTries *BACKOFF
  }
  requestAnimationFrame(rafTimer)


}
requestAnimationFrame(rafTimer)

Basically getNewMsgs is a function that makes a GET request to the /poll endpoint, gets the array of messages and displays them. Let's take a look at the requestAnimationFrame function

const BACKOFF=5000
let failedTries=0
let timeToMakeNextRequest=0
async function rafTimer(time){
  if(timeToMakeNextRequest<=time){
await getNewMsgs();
  timeToMakeNextRequest=time +INTERVAL + failedTries *BACKOFF
  }
  requestAnimationFrame(rafTimer)


}
requestAnimationFrame(rafTimer)

The time parameter of the rafTimer() function is equivalent to what is returned by performance.now().

The returned value represents the time elapsed since the time origin.

if(timeToMakeNextRequest<=time){
await getNewMsgs();
  timeToMakeNextRequest=time +INTERVAL + failedTries *BACKOFF
  }

The getNewMsgs() is called if it is timeToMakeNextRequest and the timeToMakeNextRequest variable is updated by adding the Interval and applying the linear backoff algorithm.

THE LINEAR BACKOFF ALGORITHM

Basically what is happening is this, if the request fails once, `failedTries will be incremented to 1 and the client will try to request again after 1*5000 milliseconds.

if the request fails twice, `failedTries will be incremented to 2 and the client will try to request again after 2 * 5000 milliseconds

A retry immediately when the request fails can increase the load on the system being called, if the system is already failing because it's approaching an overload. To avoid this problem, we implement our client to use backoff. This increases the time between subsequent retries, which keeps the load on the backend even.