0

I have an express server set up as follows to download torrents:

Server Side

const WebTorrent = require('webtorrent')
const client = new WebTorrent()
const express = require('express')
const app = express()
const port = 3001
const http = require('http')
const server = http.createServer(app)
const { Server } = require("socket.io");
const io = new Server(server, {
    cors: {
        origin: "http://localhost:3000"
    }
});
app.io = io

//emitting works fine, but if more than one user sends a request to this endpoint, everyone gets all messages
app.get('/test', (req, res) => {
   const link = req.query.magnetlink
   req.app.io.emit('started', true)
   client.add(link, (torrent) => {
       const name = torrent.name
       req.app.io.emit('started', true)
       torrent.on('download, () => {
           req.app.io.emit('data', () => {
                name: name,
           })
       })
       torrent.on('done', () => {
           req.app.io.emit('done', () => {
                name: name
           })
       })
   })
})
server.listen(port, () => {
  console.log(`Running on http://localhost:${port}`)
})

React Client Side

import * as React from 'react';
React.useEffect(() => {
   socket.on('started', (data) => {
       console.log(data)
       //do something here
   })
   socket.on('data', (data) => {
      console.log(data)
      //do something here
   })
}, [])

The post I got the base code from is almost 7 years old and doesn't have much information on how to do these things, along with the socket.io docs that doesn't really have any good information on this either.

There are two problems here. The first one is that all users are receiving the same messages which is a problem for my ui which adds the data to a state and it renders a part of the page with the new data. So I need every user who gets connected to get only their data that they requested.

The second problem is that using this method, I have no way of telling if the user is still connected to the route. If they are still connected I want to continue sending data, and if they aren't I want to kill the process the server is doing to retrieve and send that data. Even emitting to the server inside of one of the 'socket.on' never gets received by the server. How do I go about checking if the client is still connected so I don't waste bandwidth or waste storage space? I can't use io.on('connect') inside the route because I use it elsewhere to check if a user is online and count how many users are online. I just want to know if the user is still connected to the /test route specifically. Again, emitting works fine. I also want the messages to only be sent to that specific user connected to the /test route WHILE they are connected to the /test route. If the user refreshes their page or cancels I want to stop the data transfer on the server side which is doing things on its own.

Wamy-Dev
  • 59
  • 7
  • Problem #1 is putting a `setInterval()` inside a route handler. That means that every time the `/test` route is hit, you will start ANOTHER `setInterval()` and they will pile up and run forever. – jfriend00 Apr 18 '23 at 00:14
  • Thats just a placeholder to show that every few seconds more data is emitted. In the actual code, its data that is being passed in like a pipe. – Wamy-Dev Apr 18 '23 at 00:19
  • Can you show us the real code please? What triggers the emit and what context is that triggered from? What does "from a pipe" mean in your comment? Please show the real code, not make up code. – jfriend00 Apr 18 '23 at 00:23
  • Okay I added the real code. I am using webtorrent to get the data and serve information about the torrent. Not the torrent data itself. – Wamy-Dev Apr 18 '23 at 00:54
  • Do you have code anywhere that ever undoes the effect of `client.add()`? Like a `client.remove()`? Remember, unlike a web page, servers are long running processes so you have to clean up anything you create on behalf of a client. – jfriend00 Apr 18 '23 at 02:03
  • So, a client is NEVER connected to a route. A client requests a route and then once you send the response, that request is completely done. A client can connect to your server via webSocket or socket.io. If that's the case, then you should just implement all your logic in socket.io messages, NOT in express route handlers. If the client wants to do something that gets a response via socket.io, then just have it make that request via a socket.io message and then you will know exactly which socket.io connection to send the response back to. – jfriend00 Apr 18 '23 at 02:05
  • 1
    It's way easier to not assume some relationship between an express handler and a socket.io connection because there is not direct relationship. The express request handler is temporary and then done - the socket.io connection can be longer lasting if you want, but is really it's own thing, not part of an Express request handler. – jfriend00 Apr 18 '23 at 02:06
  • If the Express request handler is supplying the web page, then the socket.io connection will not yet be connected when the web page response is sent so there's no connection yet. If the Express request handler is an ajax call from an already loaded web page, then don't make the request via an ajax call, make it via a socket.io message and then its trivial to know which socket to send the response to. – jfriend00 Apr 18 '23 at 02:08
  • And, if you connect the lifetime of client-related things such as `client.add()` to the lifetime of the socket.io connection, then things will get cleaned up automatically when they should as you can just call `client.remove()` when the socket.io connection disconnects. – jfriend00 Apr 18 '23 at 02:09
  • yes, there is a "torrent.on("done") when the torrent is finished downloading, it gets sent as a file to the user, I just forgot to include it – Wamy-Dev Apr 18 '23 at 04:00

2 Answers2

0

The first one is that all users are receiving the same messages which is a problem for my ui which adds the data to a state and it renders a part of the page with the new data.

That's because you're doing io.emit(). That broadcasts to all connected clients. You need to do socket.emit() to the specific socket you want to send to.

The second problem is that using this method, I have no way of telling if the user is still connected to the route

Uses are never connected to a route. An express route is a temporal thing. Some client makes an http request to a route. The response gets sent. The http request/route is now over. There is no connection to a route.

A client can make a socket.io connection to a server. That makes a continuous connection between client and server that has NOTHING at all to do with a specific route. That's a connection between that specific client and the server.

If you use that socket.io connection to then request the download, then when you get the result of that download, you can send it back over the same socket that requested it. You don't have to try to match a socket with a route at all. You get a request on the socket and you send the response back on the same socket.

I don't know your torrent code, but the general structure would be like this:

// client sends socket.io getDownload message with {url: xxx} as the data to 
// initiate your torrent code

// server-side code
io.on('connection', socket => {
    socket.on('getDownload', data => {
        const url = data.url;
        client.add(url, (torrent) => {
            const name = torrent.name
            socket.emit('started', true);

            // other torrent code here
            // you can use socket.emit() here to send to this particular socket
            // When torrent is done, make sure to do client.remove() to clean up

        });
    });
    socket.on('disconnect', () => {
        // clean up any torrent stuff on behalf of this client
    });
});

Do not initiate your torrent stuff with the http URL because you don't know which socket.io connection belongs to that client making that http request or if there even is one yet.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • This cannot be used as I must use a route so it is protected by my firebase appcheck. Each route needs to be protected as I dont have any type of auth. My question has to do with routes. I would have done this if I needed to, but I cant. – Wamy-Dev Apr 18 '23 at 14:21
  • @Wamy-Dev - Well, NONE of that requirement is in the question so I suggested the best way to do things given the info provided. Your whole notion of being "connected" to a route is just wrong so you're going to have to rethink things. A client is connected to a server, not to a route. – jfriend00 Apr 18 '23 at 20:43
0

I figured out my own solution. The steps are as follows:

  1. I first sent a request to the server to /handshake, which verified the user using auth and other middlewares. This would return an OK if the user was okay to download, if not, then send whatever error (403, 500, etc).
  2. Once the client got the handshake, then I would emit a 'download' signal to socket.io with all the download options required.
  3. As the file is downloading on the server, the user would get status updates from the 'on.('download')' from the torrent.
  4. If at any time the client disconnects, the download is killed and the files deleted.
  5. Once the download is done, it is zipped and the 'torrentDone' signal is emitted to the client with file information so the user can find it later.
  6. Then whenever the user is ready, they just send the request to the server using a normal express endpoint and the file is served then deleted on the server.

Thanks for the help.

Wamy-Dev
  • 59
  • 7