*************SOCKET.IO*************

 WE WILL BE USING SOCKET.IO FOR LIVE CHAT. SOCKET.IO OFFICIAL TUTORIAL

WE WILL START WITH  1. PROJECT INITIALIZATION,

const express = require("express");
const { createServer } = require("node:http");
  const app = express();
  const server = createServer(app);

 const PORT = process.env.PORT || 3000;
  server.listen(PORT, () => {
    console.log(`app listening to ${PORT}`);
  });


2. SERVING HTML

const express = require("express");
const { createServer } = require("node:http");
const { join } = require("node:path");

  const app = express();
  const server = createServer(app);
CREATES A NEW HTTP SERVER AND TELLS IT TO USE THE EXPRESS APP TO HANDLE
INCOMING HTTP REQUESTS.

  app.get("/", (req, res) => {
    res.sendFile(join(__dirname, "index.html"));
  });


INDEX.HTML

<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Socket.IO chat</title>
<style>
body { margin: 0; padding-bottom: 3rem; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }

#form { background: rgba(0, 0, 0, 0.15); padding: 0.25rem; position: fixed; bottom: 0; left: 0; right: 0; display: flex; height: 3rem; box-sizing: border-box; backdrop-filter: blur(10px); }
#input { border: none; padding: 0 1rem; flex-grow: 1; border-radius: 2rem; margin: 0.25rem; }
#input:focus { outline: none; }
#form > button { background: #333; border: none; padding: 0 1rem; margin: 0.25rem; border-radius: 3px; outline: none; color: #fff; }

#messages { list-style-type: none; margin: 0; padding: 0; }
#messages > li { padding: 0.5rem 1rem; }
#messages > li:nth-child(odd) { background: #efefef; }
</style>
</head>
<body>
<ul id="messages"></ul>
<form id="form" action="">
<input id="input" autocomplete="off" /><button>Send</button>
</form>
</body>
</html>


3. INTEGRATING SOCKET.IO

TO INSTALL : npm install socket.io

INDEX.JS

const express = require("express");
const { createServer } = require("node:http");
const { join } = require("node:path");
const { Server } = require("socket.io");

io.on('connection', (socket) => {
console.log('a user connected');
});



INDEX.HTML

A SCRIPT TAG IS ADDED.

 <script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>

WE CAN ALSO USE A TAG INSTEAD
(OR)
<script src="/socket.io/socket.io.js"></script>

<script>
const socket = io();
</script>

A DISCONNECT BUTTON IS ADDED INORDER TO CHECK IN FURTHER CASES.

IN INDEX.HTML

  <form id="form">
      <input id="input" placeholder="type here" />
      <button type="submit">Send</button>
      <button id="toggle-btn">Disconnect</button>
    </form>

INDEX.JS

socket.on("disconnect", () => {
      console.log(`user disconnected:${socket.id}: `);
});


4. EMITTING EVENTS

WE CAN SEND AND RECEIVE ANY EVENTS.


 INDEX.HTML

 
<script>
const socket = io();
const form = document.getElementById("form");
      const input = document.getElementById("input");

form.addEventListener("submit", (e) => {
        e.preventDefault();
        if (input.value) {
socket.emit('chat message', input.value);
input.value = '';
}
});


in INDEX.JS

  io.on("connection", async (socket) => {
socket.on('chat message', (msg) => {
console.log('message: ' + msg);
});
});


5.BROADCASTING

IN ORDER TO SEND AN EVENT TO ANYONE, WE USE io.emit() 

IF YOU WANT TO SEND A MESSAGE TO EVERYONE EXCEPT FOR A CERTAIN EMITTING SOCKET, WE USE BROADCAST FLAG.

io.on('connection', (socket) => {
socket.broadcast.emit('hi');
});


io.on('connection', (socket) => {
socket.on('chat message', (msg) => {
io.emit('chat message', msg);
});
});


IN INDEX.HTML

  socket.on("chat message", (msg) => {
        const item = document.createElement("li");
        item.textContent = msg;
        messages.appendChild(item);
        window.scrollTo(0, document.body.scrollHeight);


6. HANDLING DISCONNECTIONS

TO HANDLE DISCONNECTIONS, WE USE CONNECTION STATE RECOVERY.

THIS FEATURE WILL TEMPORARILY STORE ALL THE EVENTS THAT ARE SENT BY THE SERVER AND WILL TRY TO RESTORE THE STATE OF A CLIENT WHEN IT RECONNECTS.

IN INDEX.JS

 const io = new Server(server, {
    connectionStateRecovery: {},
  });

INDEX.HTML

      <button id="toggle-btn">Disconnect</button>


      const toggleButton = document.getElementById("toggle-btn");
toggleButton.addEventListener("click", (e) => {
        e.preventDefault();
        console.log(socket);
        if (socket.connected) {
          toggleButton.innerText = "Connect";
          socket.disconnect();
        } else {
          toggleButton.innerText = "Disconnect";
          socket.connect();
        }
      });


7.SERVER DELIVERY

UPON RECONNECTION, THE MESSAGES SHOULD BE SENT IN THE SAME ORDER.

WE NEED TO STORE THE MESSAGES IN A DATABASE.

HERE WE USE MONGODB.

INDEX.JS

const mongoose = require("mongoose");


  mongoose
    .connect(
      "mongodb+srv://marsakatlaabhishek7168:abhi7168@cluster0.sixqbxk.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0"
    )
    .then(() => {
      console.log("DB connected");
    });

  const MessageSchema = mongoose.Schema({
    client_id: String,
    clientOffset: {
      type: String,
      unique: true,
    },
    content: String,
  });
  const Message = new mongoose.model("Message", MessageSchema);






socket.on("chat message", async (msg, clientOffset, callback) => {
      let newMessage;
      try {
        newMessage = await Message.create({
          client_id: socket.id,
          clientOffset: clientOffset,
          content: msg,
        });

        console.log(newMessage);
}
catch (e) {
return;
}
      io.emit("chat message", msg, newMessage._id);

INDEX.HTML

 const socket = io({
        auth: {
          serverOffset: 0,
        }




      socket.on("chat message", (msg, serverOffset) => {
        const item = document.createElement("li");
        item.textContent = msg;
        messages.appendChild(item);
        window.scrollTo(0, document.body.scrollHeight);
        socket.auth.serverOffset = serverOffset;
      });

 ALSO IN INDEX.JS

IF SOCKET IS NOT RECOVERED.

if (!socket.recovered) {
      try {
        const serverOffset = socket.handshake.auth.serverOffset;
SERVER OFFSET - ID OF THE LAST MESSAGE SEEN BY USER

        let query = {};
        if (serverOffset && mongoose.Types.ObjectId.isValid(serverOffset)) {
IF A VALID SERVEROFFSET EXISTS , ALL THE MESSAGES AFTER THAT (GREATER THAN THAT ID) ARE
FETCHED
          query._id = { $gt: mongoose.Types.ObjectId(serverOffset) };
        }
        const messages = await Message.find({ query }).sort({ _id: 1 });
SORTING MESSAGES OLDER TO NEWER

        messages.forEach((messages) => {
          socket.emit("chat message", messages.content, messages._id);
        });
      } catch (e) {
        console.log("something went wrong!");
      }
    }


8. CLIENT DELIVERY

WHEN A CLIENT GETS DISCONNECTED, ANY CALL TO SOCKET.EMIT() IS BUFFERED UNTIL RECONNECTION.

BUT IN SOME CASES, MESSAGES MIGHT BE LOST.

 - SERVER CRASHED

- DB TEMPORARILY NOT AVAILABLE

 WE CAN USE RETIRES OPTION.

INDEX.HTML

 const socket = io({
        auth: {
          serverOffset: 0,
        },
        // enable retries
        ackTimeout: 10000,
        retries: 3,
      })


form.addEventListener("submit", (e) => {
        e.preventDefault();
        if (input.value) {
          const clientOffset = `${socket.id}-${counter++}`;
          socket.emit("chat message", input.value, clientOffset);
          input.value = "";
        }
      });


INDEX.JS

 socket.on("chat message", async (msg, clientOffset, callback) => {
      let newMessage;
      try {
        newMessage = await Message.create({
          client_id: socket.id,
          clientOffset: clientOffset,
          content: msg,
        });

        console.log(newMessage);
      } catch (e) {
MONGO DB CONSTRAINT
        if (e.code === 11000) {
          callback();
        } else {
// NOTHING HERE, USER WAITS UNTIL RECONNECTION
        }
        return;
      }
      io.emit("chat message", msg, newMessage._id);
      callback();
    });

9. SCALING HORIZONTALLY

WE MIGHT HAVE TEMPORARY NETWORK INTERRUPTIONS . INORDER TO SUPPORT THOUSANDS OF CLIENTS, WE USE HORIZONTAL AND VERTICAL SCALING.

HORIZONTAL SCALING (SCALING OUT) - ADDING NEW SERVERS

VERTICAL SCALING (SCALING IN) - ADDING MORE RESOURCES

BY DEFAULT NODE.JS RUNS JS CODE IN A SINGLE THREAD, WHICH MEANS THAT EVEN WITH A 32 CPU CORE ONLY ONE CORE WILL BE USED.

WE WILL ALSO NEED A WAY TO FORWARD EVENTS BETWEEN THE SOCKET.IO SERVERS.

THIS COMPONENT USED TO FORWARD EVENTS IS CALLED ADAPTER.

INDEX.JS

npm install @socket.io/cluster-adapter

IN INDEX.JS

onst { availableParallelism } = require("node:os");
const cluster = require("node:cluster");
const { createAdapter, setupPrimary } = require("@socket.io/cluster-adapter");
const { on } = require("node:events");

if (cluster.isPrimary) {
  const numCPUs = availableParallelism();
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork({
      PORT: 3000 + i,
    });
  }

  return setupPrimary();
}



  const io = new Server(server, {
    connectionStateRecovery: {},
    adapter: createAdapter(),
  });

 WE HAVE ALSO IMPLEMENTED HOW TO DISPLAY USER NAMES (IN OUR CASE SOCKET ID'S) WHEN A NEW USER JOINS THE CHAT.

INDEX.JS

 let userList = [];

  io.on("connection", async (socket) => {
    console.log(`user connected: ${socket.id}`);
    userList.push(socket.id);
    console.log(userList);
    io.emit("userList", userList);
    socket.broadcast.emit("system message", `${socket.id} has joined the chat`);

    socket.on("disconnect", () => {
      console.log(`user disconnected:${socket.id}: `);
      userList.pop(socket.id);
      socket.broadcast.emit("system message", `${socket.id} has left the chat`);
      io.emit("userList", userList);
    });


INDEX.HTML

 <body>
    <nav class="navbar bg-body-tertiary">
      <div class="container-fluid">
        <span class="navbar-brand mb-0 h5" id="userList"></span>
      </div>
    </nav>
    <ul id="messages"></ul>


      const userListElement = document.getElementById("userList");

      socket.on("system message", (msg) => {
        const item = document.createElement("li");
        item.textContent = msg;
        item.style.fontStyle = "italic";
        item.style.color = "grey";
        item.style.textAlign = "center";
        messages.append(item);
      });


NOTE : THERE WERE MANY ERRORS AFTER FEW TESTS

THE FINAL CODE:

INDEX.HTML

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>socket</title>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
      rel="stylesheet"
    />
    <style>
      body {
        margin: 0;
        padding-bottom: 3rem;
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
          Helvetica, Arial, sans-serif;
      }

      #form {
        background: rgba(0, 0, 0, 0.15);
        padding: 0.25rem;
        position: fixed;
        bottom: 0;
        left: 0;
        right: 0;
        display: flex;
        height: 3rem;
        box-sizing: border-box;
        backdrop-filter: blur(10px);
      }
      #input {
        border: none;
        padding: 0 1rem;
        flex-grow: 1;
        border-radius: 2rem;
        margin: 0.25rem;
      }
      #input:focus {
        outline: none;
      }
      #form > button {
        background: #333;
        border: none;
        padding: 0 1rem;
        margin: 0.25rem;
        border-radius: 3px;
        outline: none;
        color: #fff;
      }

      #messages {
        list-style-type: none;
        margin: 0;
        padding: 0;
      }
      #messages > li {
        padding: 0.5rem 1rem;
      }
      #messages > li:nth-child(odd) {
        background: #efefef;
      }
    </style>
  </head>
  <body>
    <nav class="navbar bg-body-tertiary">
      <div class="container-fluid">
        <span class="navbar-brand mb-0 h5" id="userList"></span>
      </div>
    </nav>
    <ul id="messages"></ul>
    <form id="form">
      <input id="input" placeholder="type here" />
      <button type="submit">Send</button>
      <button id="toggle-btn">Disconnect</button>
    </form>
    <script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
    <script>
      let counter = 0;
      let clientServerOffset = null;
      const socket = io({
        ackTimeout: 10000,
        retries: 3,
        auth: {},
      });

      const form = document.getElementById("form");
      const input = document.getElementById("input");
      const messages = document.getElementById("messages");
      const toggleButton = document.getElementById("toggle-btn");
      const userListElement = document.getElementById("userList");

      toggleButton.addEventListener("click", (e) => {
        e.preventDefault();
        console.log(socket);
        if (socket.connected) {
          toggleButton.innerText = "Connect";
          socket.disconnect();
        } else {
          toggleButton.innerText = "Disconnect";
          socket.auth = { serverOffset: clientServerOffset || null };
          socket.connect();
        }
      });
      form.addEventListener("submit", (e) => {
        e.preventDefault();
        if (input.value) {
          const clientOffset = `${socket.id}-${counter++}`;
          socket.emit("chat message", input.value, clientOffset, () => {
            console.log("Message delivered");
          });
          input.value = "";
        }
      });

      socket.on("userList", (userList) => {
        console.log(userList);
        console.log(userList.length);

        if (userList.length === 0) {
          userListElement.textContent = "No users online";
        } else if (userList.length === 1) {
          userListElement.textContent = `${userList[0]} is online`;
        } else {
          userListElement.textContent = `${userList.join(",")} are online`;
        }
      });
      socket.on("chat message", (msg, serverOffset) => {
        const item = document.createElement("li");
        item.textContent = msg;
        messages.appendChild(item);
        window.scrollTo(0, document.body.scrollHeight);
        clientServerOffset = serverOffset;
        console.log(clientServerOffset);
      });
      socket.on("system message", (msg) => {
        const item = document.createElement("li");
        item.textContent = msg;
        item.style.fontStyle = "italic";
        item.style.color = "grey";
        item.style.textAlign = "center";
        messages.append(item);
      });
    </script>
  </body>
</html>

INDEX.JS

const express = require("express");
const { createServer } = require("node:http");
const { join } = require("node:path");
const { Server } = require("socket.io");
const mongoose = require("mongoose");
const { type } = require("node:os");
const { availableParallelism } = require("node:os");
const cluster = require("node:cluster");
const { createAdapter, setupPrimary } = require("@socket.io/cluster-adapter");
const { on } = require("node:events");

if (cluster.isPrimary) {
  const numCPUs = availableParallelism();
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork({
      PORT: 3000 + i,
    });
  }

  return setupPrimary();
}

async function main() {
  mongoose
    .connect(
      "mongodb+srv://marsakatlaabhishek7168:abhi7168@cluster0.sixqbxk.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0"
    )
    .then(() => {
      console.log("DB connected");
    });
  const MessageSchema = mongoose.Schema({
    client_id: String,
    clientOffset: {
      type: String,
      unique: true,
    },
    content: String,
  });
  const Message = new mongoose.model("Message", MessageSchema);

  const app = express();
  const server = createServer(app);

  const io = new Server(server, {
    connectionStateRecovery: {},
    adapter: createAdapter(),
  });

  app.get("/", (req, res) => {
    res.sendFile(join(__dirname, "index.html"));
  });

  let userList = [];
  io.on("connection", async (socket) => {
    console.log(socket.handshake.auth.serverOffset);
    console.log(`user connected: ${socket.id}`);
    userList.push(socket.id);
    io.emit("userList", userList);
    socket.broadcast.emit("system message", `${socket.id} has joined the chat`);

    socket.on("disconnect", () => {
      console.log(`user disconnected:${socket.id}`);
      userList = userList.filter((id) => id !== socket.id);
      socket.broadcast.emit("system message", `${socket.id} has left the chat`);
      io.emit("userList", userList);
    });

    socket.on("chat message", async (msg, clientOffset, callback) => {
      let newMessage;
      try {
        newMessage = await Message.create({
          client_id: socket.id,
          clientOffset: clientOffset,
          content: msg,
        });
        console.log(newMessage);
      } catch (e) {
        if (e.code === 11000) {
          callback();
        }
        return;
      }
      io.emit("chat message", msg, newMessage._id.toString());
      callback();
    });

    if (!socket.recovered) {
      try {
        let serverOffset = socket.handshake.auth.serverOffset;
        console.log(serverOffset);
        let query = {};

        if(serverOffset && mongoose.isValidObjectId(serverOffset)){
          query._id = { $gt:new mongoose.Types.ObjectId(serverOffset) };
        }
        const messages = await Message.find(query).sort({ clientOffset: 1 });
        messages.forEach((message) => {
          socket.emit("chat message", message.content,  message._id.toString());
        });
      } catch (e) {
        console.log(e);
        console.log("something went wrong!");
      }
    }
  });

  const PORT = process.env.PORT || 3000;
  server.listen(PORT, () => {
    console.log(`app listening to ${PORT}`);
  });
}
main();

































Comments

Popular posts from this blog

MIDDLEWARE.JS

MODELS

AUTHENTICATION PAGE