dispatch a WebSocket message within a route using Express.js

The Objective:

Imagine a bustling marketplace with multiple shops. I'm working on a dedicated page

localhost:3000/livePurchases/:storeId
for shop owners to receive real-time notifications whenever they make a new sale.

https://i.stack.imgur.com/VdNzjm.png

https://i.stack.imgur.com/rf8u3m.png

alert('you received a new purchase')
should be activated by a WebSocket every time an item is purchased.

The Challenge:

I am faced with the task of setting up WebSockets on my express server in such a way that the websocket can be triggered from a different part of my code than where the websocket server was initially established. Unfortunately, this process has left me puzzled.

The route /newPurchase/:storeId will be accessed by the customer's browser after a successful purchase. The websocket should transmit a message within the code of the route

"/newPurchase/:storeId"
(backend) to the websocket located at
"/livePurchases/:storeId"
(frontend), allowing the shop owner to monitor live purchases.

app.js

const express = require("express");

module.exports = (config) => {
  const app = express();

  app.post("/newPurchase/:storeId", (req, res, next) => {
    const { storeId } = req.params;
    // trigger websocket message to `localhost:3000/livePurchases/:storeId`
    // when client requests this route

  });

  return app;
};

However, since app.js is exported and executed from another script, namely www.js, additional steps are necessary. This approach is typically employed to establish a connection to a database before running the application:

www.js

const app = require("../server/app")();

const port = process.env.PORT || "4000";
app.set("port", port);

app
  .listen(port)
  .on("listening", () =>
    console.log("info", `HTTP server listening on port ${port}`)
  );

module.exports = app;

This means that the WebSocket server setup needs to be included in www.js.

The following code snippet showcases a notifier service obtained from this tutorial. While it appeared to address similar issues, it lacked clarity on implementation details:

NotifierService.js

const url = require("url");
const { Server } = require("ws");

class NotifierService {
  constructor() {
    this.connections = new Map();
  }

  connect(server) {
    this.server = new Server({ noServer: true });
    this.interval = setInterval(this.checkAll.bind(this), 10000);
    this.server.on("close", this.close.bind(this));
    this.server.on("connection", this.add.bind(this));
    server.on("upgrade", (request, socket, head) => {
      console.log("ws upgrade");
      const id = url.parse(request.url, true).query.storeId;

      if (id) {
        this.server.handleUpgrade(request, socket, head, (ws) =>
          this.server.emit("connection", id, ws)
        );
      } else {
        socket.destroy();
      }
    });
  }

  add(id, socket) {
    console.log("ws add");
    socket.isAlive = true;
    socket.on("pong", () => (socket.isAlive = true));
    socket.on("close", this.remove.bind(this, id));
    this.connections.set(id, socket);
  }

  send(id, message) {
    console.log("ws sending message");

    const connection = this.connections.get(id);

    connection.send(JSON.stringify(message));
  }

  broadcast(message) {
    console.log("ws broadcast");
    this.connections.forEach((connection) =>
      connection.send(JSON.stringify(message))
    );
  }

  isAlive(id) {
    return !!this.connections.get(id);
  }

  checkAll() {
    this.connections.forEach((connection) => {
      if (!connection.isAlive) {
        return connection.terminate();
      }

      connection.isAlive = false;
      connection.ping("");
    });
  }

  remove(id) {
    this.connections.delete(id);
  }

  close() {
    clearInterval(this.interval);
  }
}

module.exports = NotifierService;

Progression with Implementing the `NotifierService`

I integrated the websocket server utilizing the NotifierService within www.js

www.js with websockets added

const app = require("../server/app")();
const NotifierService = require("../server/NotifierService.js");
const notifier = new NotifierService();
const http = require("http");
const server = http.createServer(app);
notifier.connect(server);
const port = process.env.PORT || "4000";
app.set("port", port);

server
  .listen(port)
  .on("listening", () =>
    console.log("info", `HTTP server listening on port ${port}`)
  );

module.exports = app;

Now, the challenge lies in sending the websocket message from the /newPurchase route in app.js on the backend? If I were to create a new instance of NotifierService in app.js to utilize the notifierService.send method in the /newPurchase route, this would result in the new NotifierService instance lacking access to the existing websocket connections initiated in www.js.

Front End:

App.js

import React from "react";

import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import LiveStorePurchases from "./LiveStorePurchases";

function App(props) {
  return (
    <div className="App">
      <Router>
        <Switch>
          <Route exact path="/livePurchases/:storeId">
            <LiveStorePurchases />
          </Route>
        </Switch>
      </Router>
    </div>
  );
}

export default App;

LivePurchaseServer.js

import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";

export default function LiveStorePurchases() {
  let { storeId } = useParams();
  const URL = "ws://127.0.0.1:4000?storeId=" + storeId;

  const [ws, setWs] = useState(new WebSocket(URL));

  useEffect(() => {
    ws.onopen = (e) => {
      newFunction(e);

      function newFunction(e) {
        alert("WebSocket Connected");
      }
    };

    ws.onmessage = (e) => {
      const message = e.data;
      alert(message);
    };

    return () => {
      ws.onclose = () => {
        alert("WebSocket Disconnected");
        setWs(new WebSocket(URL));
      };
    };
  }, [ws.onmessage, ws.onopen, ws.onclose, ws, URL]);

  return (
    <div
      style={{
        color: "red",
        fontSize: "4rem",
      }}
    >
      store: {storeId}
    </div>
  );
}

Answer №1

app.js:

After some refactoring, I successfully moved the websocket instance from www.js to app.js. This allowed me to easily pass the instance to different routes within the application.

const express = require("express");
const NotifierService = require("../server/NotifierService.js");
const notifier = new NotifierService();
const http = require("http");
const routes = require("./routes");

module.exports = (config) => {
  const app = express();
  const server = http.createServer(app); // creating websocket
  notifier.connect(server);              // connecting websocket in app.js

  //   The route POST /newPurchase was moved to routes.js to demonstrate 
  //   how the notifier instance can be passed around to different routes
  app.use(routes(notifier));

  return server;
};

routes.js:

To show the flexibility of moving the notifier instance, I created a separate file routes.js where any route can access and use the notifier service.

const express = require("express");

const router = express.Router();

// The "/newPurchase/:id" route was moved here to showcase 
// how the notifier instance can be utilized from any route.

module.exports = (webSocketNotifier) => {
  router.post("/newPurchase/:id", (req, res, next) => {
    webSocketNotifier.send(req.params.id, "purchase made");
    res.status(200).send();
  });
  return router;
};

www.js:

const server = require("../server/app")();

const port = process.env.PORT || "4000";

server
  .listen(port)
  .on("listening", () =>
    console.log("info", `HTTP server listening on port ${port}`)
  );

module.exports = server;

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Importing an external JSON file into a ChartJs chart

As a newcomer to using libraries for drawing charts in JavaScript, I recently started playing around with Chartjs. However, I'm having trouble figuring out how to use getJson or any other method to load my json object and update the labels and data. I ...

Recording Audio Using ReactJS

I am exploring ways to record audio using ReactJS and save it on my Node server. Initially, I attempted to utilize the "react-audio-recorder" module but encountered issues when attempting to record multiple audios consecutively. Additionally, I experiment ...

Successfully updating a document with Mongoose findByIdAndUpdate results in an error being returned

findByIdAndUpdate() function in my code successfully updates a document, but unexpectedly returns an error that I am having trouble understanding. Below is the schema that I am working with: const userSchema = mongoose.Schema({ phone: String, pas ...

The CSS styles are failing to load properly for the antd Menu option

I am working with React using the Next.js framework, and I've integrated the antd npm package for components like tables and menus. However, I'm facing an issue where the CSS is not loading for these controls. What could be causing this problem? ...

Strategies for displaying error messages in case of zero search results

I am currently developing a note-taking application and facing an issue with displaying error messages. The error message is being shown even when there are no search results, which is not the intended behavior. Can someone help me identify what I am doing ...

MongoDB facing difficulties in updating the database

Seeking help with my MongoDB setup as I am still new to it. I have a database where card data is stored and need to update counts when users like cards, resulting in a total likes number. I'm facing an issue where I keep getting a 400 error response ...

Is the CSS scale activated by mouseover or click?

My CSS code successfully scales images, but the issue is that it affects every image on the page. I am looking for a solution to apply this CSS only when the user hovers over or clicks on an image. The challenge is that images are added by non-technical w ...

What's the best way to ensure that the theme state remains persistent when navigating, refreshing, or revisiting a page in the browser?

Is there a way to ensure that my light/dark theme settings remain persistent when users reload the page, navigate to a new page, or use the browser's back button? The current behavior is unreliable and changes unexpectedly. This is the index.js file ...

"Getting Started with Respond.js: A Step-by-Step

I've been struggling to find clear instructions on how to properly set up respond.js. Should I just unzip it into the htdocs folder, or do I only need respond.min.js in there? Then, do I simply reference the file like this... <script src="respon ...

What might be causing the issue preventing me from successfully publishing my package on npmjs?

After setting up an account on npmjs.com, I decided to follow the how-to-npm tutorial using the command line (linux). Currently, I am stuck at the point where I need to publish my test module. However, every time I try, I encounter this error message: You ...

Looping through a series of URLs in AngularJS and performing an $

Currently, I am facing an issue while using angular.js with a C# web service. My requirement is to increment ng-repeat item by item in order to display updated data to the user. To achieve this, I attempted to utilize $http.get within a loop to refresh t ...

Tips for creating a personalized callback within a user function using JavaScript

Utilizing callbacks is a common practice when working with third-party libraries like jQuery. However, I have encountered a situation where I need to implement my own callback function. Consider the following snippet from my current code: // Get All Rates ...

Raycasting for collision detection is not very precise

I am facing a challenge in my project where I have multiple irregular shapes like triangles, trapezoids, and other custom designs in a 2D scene all located on the Y=0 axis. I am currently working on writing code for collision detection between these shapes ...

A "Uncaught TypeError" error occurs when trying to execute a function using the dollar sign

After successfully recognizing the hover function, the console displays an error message: Uncaught TypeError: $ is not a function <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script> <script> $(docume ...

When using React JS to sign in with LinkedIn, users are redirected to a 404 error page on a hosted website but not when testing

I've been working on a React JS application and attempting to incorporate Signin with LinkedIn. The functionality works perfectly on my localhost, but when I deploy the app online, a popup appears, I log in, and then it redirects to a 404 page with th ...

The function FileReader() is not functioning properly within a Vue computed property

I'm attempting to display a set of image thumbnails by dragging images onto the screen. Here is an example of my data structure: data() { return { files: [Image1, Image2, Image3] } } ...where each Image is in a blob format. Below is my co ...

Strengthening JavaScript Security

Throughout the past few years, I have delved into various javascript libraries like Raphael.js and D3, experimenting with animations sourced from different corners of the internet for my own learning. I've obtained code snippets from Git repositories ...

Difficulty loading AngularJS 1.3 page on Internet Explorer 8

Being an avid user of Angular, it pains me to even bring up the topic of IE8, a browser that many consider to be pure evil and deserving of extinction. Despite my reservations, I am experiencing difficulties with loading Angular 1.3 in IE8. The page break ...

Utilize state objects and child components by accessing sub-values within the object

I have a Dropzone component where multiple uploads can happen simultaneously and I want to display the progress of each upload. Within my Dropzone component, there is a state array called uploads: const [uploads, setUploads] = useState([]) Each element i ...

Ember - Issue with Page Display

Currently tackling my first ember application, which consists of two pages: the Welcome page and a page displaying student details. To accomplish this, I have established two routes named index and studentdb. Unfortunately, I'm encountering an issue w ...