Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Documentation] Exposing WebSocketServer for server-side access #20

Open
punteroo opened this issue Feb 21, 2024 · 0 comments
Open

[Documentation] Exposing WebSocketServer for server-side access #20

punteroo opened this issue Feb 21, 2024 · 0 comments

Comments

@punteroo
Copy link

Hello!

I wanted to provide some more insight on how I accomplished broadcasting events asynchronously using this excellent package, props to @apteryxxyz for this beautiful implementation.
For my example, these are the package versions used at the moment of writing this.

  • next: 14.1.0
  • next-ws: 1.0.1
  • ws: 8.16.0

Exposing WebSocketServer to global

ws exposes a clients list on the WebSocket server. Normally, using next-ws one would only access the WebSocket server inside the exported SOCKET route.
To expose the WebSocket server to different parts of your Next application, export it as follows:

// app/api/socket/route.ts
import { WebSocket, WebSocketServer } from "ws";
import { IncomingMessage } from "http";

export function SOCKET(
  client: WebSocket,
  request: IncomingMessage,
  server: WebSocketServer
) {
  (global as any)["wsServer"] = server;
}

Using Node's global accesor, and thanks to the patch done by next-ws before-hand, this WebSocket server object persists itself statelessly on the entire application. This is good even for Next applications where the output is standalone as it is most common when dockerizing said app.

Using this method, one maintains Typescript support and avoids utilizing a custom Next server.

I'm finding I have a somewhat similar use-case. I'm building an app that needs an external source to be able to send messages to the WebSocket server, but I'm hitting a wall in getting that working. I tried the above solution, and was able to get it working locally, but since my project is built in `standalone` mode (for Docker) it wasn't super clear how to use the custom server alongside the `server.js` that gets generated by Next. Also didn't really love losing TypeScript support to have a custom server.

Originally posted by @Alex-Mastin in #8 (comment)

Broadcast to Everyone

WebSocketServer provides a clients list. If you want, for example, wait for an incoming webhook message in order to broadcast something to all connected clients in the WebSocket, simply access the globalized object and stream it out to said clients.

// app/api/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";
import { WebSocket } from "ws";
import { WebSocketServer } from "ws";

export async function POST(req: NextRequest) {
  try {
    // Fetch the WebSocket server.
    const webSocket = (global as any)?.["wsServer"] as WebSocketServer;

    if (!webSocket)
      return NextResponse.json(
        { status: false },
        {
          status: 500,
        }
      );

    // Parse the client set into an array.
    const clients = Array.from(
      webSocket.clients
    );

    // Emit the message to the client.
   for (const client of clients)
      client.send('Hello from the webhook!');

    return NextResponse.json({ status: true });
  } catch (e) {
    console.error(e);
    throw e;
  }
}

You can broadcast any message you wish via this method, fully async and never losing connectivity. ws allows any Buffer derived data to go through, so as long as your data falls into this category you should be good to go (i.e: strings, files)

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/ws/index.d.ts#L20-L36

Broadcasting to Specific Clients

As next-ws uses ws for connectivity, one can send query parameters through the connection string for the exported WebSocket server route. This is great to identify clients via an ID, or off-load some data to their client connection to later check when streaming messages asynchronously on the server-side.

If you're using Typescript: ws must be augmented to allow you to set more properties on the WebSocket client, an example for this would be allowing clients to have an ID. As such:

// types/ws/index.d.ts
import WebSocket from "ws";

interface WebSocketClient {
  id: string;
}

declare module "ws" {
  interface WebSocket extends WebSocketClient {}
  namespace WebSocket {
    type id = string;
  }
}

Now you can set IDs for incoming WebSocket connection clients which are fully readable on the WebSocket server clients list. This is an example on how to accomplish a sort-of basic selective broadcast.

Client-side

Note: This is not a secure way to send parameters to the WebSocket server. This is just as an example implementation.

/app/components/SomeComponent.tsx
"use client";

import { WebSocketProvider } from "next-ws/client";

// As an example, userId would be 12345
export default function SomeComponent({ children, userId }: { children: React.Node, userId: string }) {
  return (
    <WebSocketProvider url={`ws://localhost:3000/api/socket?userId=${userId}`}>
      {...children}
    </WebSocketProvider>
  );
}

Server-side

Load the provided query parameters from the request object. You can validate stuff here and then assign the client the provided ID.

import { WebSocket, WebSocketServer } from "ws";
import { IncomingMessage } from "http";

export function SOCKET(
  client: WebSocket,
  request: IncomingMessage,
  server: WebSocketServer
) {
  // Obtain query parameters.
  const query = new URLSearchParams(request.url!.split("?")[1]);

  // Assign the provided userId to the client.
  client["id"] = query.get("userId") ?? "Unknown";

  (global as any)["wsServer"] = server;
}

Whenever you wish to broadcast something specific, access the client.id property when looping through the WebSocket server clients list.

// app/api/webhook/route.ts
import { NextRequest, NextResponse } from "next/server";
import { WebSocket } from "ws";
import { WebSocketServer } from "ws";

export async function POST(req: NextRequest) {
  try {
    // Fetch the WebSocket server.
    const webSocket = (global as any)?.["wsServer"] as WebSocketServer;

    if (!webSocket)
      return NextResponse.json(
        { status: false },
        {
          status: 500,
        }
      );

    // Parse the client set into an array.
    // Typescript type augmentation done to allow for the use of the `id` property.
    const clients: Array<WebSocket> = Array.from(
      webSocket.clients as Set<WebSocket>
    );

    // Emit the message to the client.
   for (const client of clients) {
      console.log(client.id); // 12345
      client.send('Hello from the webhook!');
    }

    return NextResponse.json({ status: true });
  } catch (e) {
    console.error(e);
    throw e;
  }
}
@punteroo punteroo changed the title [Documentation] Broadcast events to connected clients [Documentation] Exposing WebSocketServer for server-side access Feb 21, 2024
@apteryxxyz apteryxxyz pinned this issue Mar 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant