node.jsexpresswebsocketbackendserver-sent-events

How to forward data from a WebSocket connection to a Server-Sent Event in Express?


When a client requests an endpoint on my Express.js / Node.js server, the server connects to an external WebSockets endpoint, and receives data in several messages. I want to be able to forward the data received by that connection to the client as a Server-Sent Event.

So the connections I need would look like this: Client <-Server-Sent Event-> Node/Express server <-WebSocket-> External server.

The socket connection is encapsulated in a simple class:

class SocketClient {
  private socket: WebSocket;

  constructor() {
    this.socket = new WebSocket("ws://example.com");
    this.socket.send("something");
    socket.addEventListener("message", (event) => {
      console.log("Message from server ", event.data);
    });
  }
}

, and on the Express side of things is a standard HTTP endpoint, where I want to use the standard write and end methods of the Node Response class:

const app = express();

app.get('/', (req, res) => {
  const client = new SocketClient();

  while (await client.isDataAvailable()) {
    res.write(await client.getNextDataChunk());
  }
  res.end();
});

I am struggling in forwarding the data from one asynchronous connection to the other. In the code above, that would mean implementing the isDataAvailable and getNextDataChunk methods on SocketClient, or something else that would achieve a similar result.


Solution

  • I have solved it by wrapping WebSocket messages in Promises that only resolve when the connection is opened, or when last message from the socket connection is received (this can be changed to fit your needs, to for example resolve when the connection is closed):

    class SocketClient {
        private socket: WebSocket;
    
        // Important: only resolve promise when connection opened
        public async connectToSocket() {
            return new Promise<void>((resolve, reject) => {
                const socket = new WebSocket("wss://example.com");
                this.socket = socket;
    
                socket.onopen = (event) => {
                    resolve();
                }
            });
        }
    
        public async getAsyncData(callback: (chunk: string | Buffer | Uint8Array) => Promise<void>): Promise<void> {
            // Send anything from client side as needed
            this.socket.send("...");
    
            // Listen to responses and asyncronously call the callback
            this.socket?.addEventListener("message", async (event) => {
                if (event.data) {
                    await callback(event.data);
                }
            });
    
            await this.finishGettingData();
        }
    
        // Importantly, this only resolves after the last message
        private async finishGettingData() {
            return new Promise<boolean>((resolve) => {
                (this.socket as WebSocket).addEventListener("message", (event) => {
                    // Check for end message, depends on your specific case also
                    // For this example, checking for message content
                    if (event.data === "Final message") {
                        resolve(true);
                    }
                });
            });
        }
    }
    
    const app = express();
    
    app.get('/', async (req, res) => {
        const client = new SocketClient();
    
        await client.connectToSocket();
    
        // This will only resolve when ending message is received.
        await client.getAsyncData(async (chunk) => {
            console.log("Writing a chunk to client, size", chunk.length);
            res.write(chunk);
        });
        res.end();
    });