1

The Goal:

Let's say we have a marketplace full of shops. I'm creating a specific page localhost:3000/livePurchases/:storeId for a shop owner to monitor live notifications of when they got a new purchase.

enter image description here

enter image description here

alert('you received a new purchase') should be triggered by a WebSocket when an item is purchased.

The Problem:

I need to set up WebSockets on my express server so that the websocket can be triggered somewhere in my code different from where the websocket server was set up. But I don't understand how to do this.

The route /newPurchase/:storeId would be requested by the browser of a customer after they successfully purchase an item. The websocket should send a message within the code of the route "/newPurchase/:storeId" (backend) to the websocket on "/livePurchases/:storeId" (frontend) where the shop owner can 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;
};

But app.js is exported and run from another script, www.js. In real scenarios this is to connect a database before running the app.:

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;

So that means that the web socket server needs to be set up in www.js.

Below is a notifier service I got it from this tutorial, which seemed like it was trying to solve the problem I have, but it didn't explain how to implement it. It is a class that handles the websocket.

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;

Where I left off implementing the `NotifierService`

I added the websocket server with the NotifierService in 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;

But now how do I send the websocket message from the /newPurchase route in app.js on the backend? If I create a new instance of NotifierService in app.js in order to use the notifierService.send method in the /newPurchase route, then the new NotifierService instance won't have access to the websocket connections because it would be a different instance than the one initiated on 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>
  );
}

7
  • First off, is the /newPurchase/:storeId route being sent from Javascript in your web page? If not and it's a regular form post, then you can't send to a webSocket in that page anyway because the page will reload and it will have a new/different webSocket connection. Commented Dec 16, 2021 at 0:07
  • The request to the localhost:4000/newPurchase/:storeId route would come from a page where a customer checks out to indicate that a purchase was made. I wouldn't be sending any websocket messages back to that checkout page. I would be sending the websocket message to a different page that the owner of the shop may be monitoring- localhost:3000/livePurchases/:storeId. So the message would be initiated in the code of the route /newPurchase/:storeId but it would be sent to the page /livePurchases/:storeId. Commented Dec 16, 2021 at 2:58
  • Then you need to find some way on the server to store any webSocket socket or sockets that belongs to a store owner so you can iterate through them and send a message on them. Presumably you'd be monitoring incoming webSockets connections, figure out which ones belong to a store owner and save them somewhere and also be monitoring disconnects and removing them from the stored list (if they were in that list). Commented Dec 16, 2021 at 3:44
  • If you use socket.io instead of plain webSocket, this would be a perfect application for their "rooms" concept where you would add a store owner to a room when they connect and you can then broadcast to any connections in that room at any time from anywhere in the server with io.to(someRoomName).emit(...). Commented Dec 16, 2021 at 3:46
  • Thanks. Yeah I understand sending it to the websocket associated with the correct user is part of the process too. I'll need to figure that out. I think the tutorial I posted sort of addresses that. However the part I'm stuck on now is how do I access the websocket instance in the post request? Because it was created in www.js: const notifier = new NotifierService();.... notifier.connect(server). How to I access it in app.js? Commented Dec 16, 2021 at 3:53

1 Answer 1

1

app.js:

I was able to move the websocket instance to app.js instead of www.js. Then I simply passed that instance around to other routes.

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); // websocket created
  notifier.connect(server);              // and connected here in app.js

  //   I moved POST /newPurchase to routes.js in order
  //   to demonstrate how the notifier instance can be
  //   passed around to different routes
  app.use(routes(notifier));

  return server;
};

routes.js:

I created a routes file routes.js to show that you could move the notifier instance around and call it from any route.

const express = require("express");

const router = express.Router();

// I moved "/newPurchase/:id" to this routes.js file to show that
// I could move the notifier instance around.

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;
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.