NetClient

A lightweight WebSocket networking library for browser games. Rooms, relays, host management, tags, metadata, and binary support — all in one small file.

Zero Dependencies WebSocket ES Module Browser
▶  Try the Live Demo
🚪

Room Management

Create, join, and leave rooms with max client limits and privacy controls.

📡

Relay Messaging

Broadcast JSON payloads to all players or target a specific player or the host.

👑

Host Handoff

Automatic host reassignment when the current host disconnects.

Binary Support

Send raw ArrayBuffers for high-frequency position updates and game state.

🏷️

Tags & Metadata

Tag rooms for filtering, and attach custom metadata like lobby names.

🔌

Simple Events

Clean net.on() event API — no boilerplate, just callbacks.

Installation

NetClient is a single ES module file with no dependencies. Drop it into your project and import it.

  1. Download netClient.js Copy netClient.js into your project folder alongside your HTML file.
  2. Import it as an ES module
    your-game.js
    import NetClient from "./netClient.js";
    Note Your script tag must use type="module" for ES module imports to work.
  3. Add type="module" to your script tag
    index.html
    <script type="module" src="./your-game.js"></script>
  4. Point to a server NetClient connects to any WebSocket server that speaks its protocol. You can self-host server.js on Node.js, or deploy it to a free service like Render.
    Free hosting tip Render's free tier supports persistent WebSocket connections and is a great fit for hobby game servers. Deploy server.js with node server.js.

Quick Start

Get two players connected and talking in under 20 lines.

game.js
import NetClient from "./netClient.js";

const net = new NetClient("wss://your-server.onrender.com", "myGame");

net.connect();

net.on("connected", () => {
  // Connection open — create a room
  net.createRoom([], 4, false);
});

net.on("roomCreated", (roomId) => {
  console.log("Room ready:", roomId); // Share this ID with others
});

net.on("playerJoined", (playerId) => {
  // Broadcast your current state to the new player
  net.sendRelay({ x: player.x, y: player.y, color: player.color });
});

net.on("relay", (fromId, payload) => {
  // Another player sent us their state
  updatePlayer(fromId, payload);
});

Constructor

NEW new NetClient(url, gameName?)

Creates a new NetClient instance. Does not connect until connect() is called.

ParameterTypeDescription
urlstringWebSocket server URL, e.g. "wss://your-server.onrender.com"
gameNamestringOptional. Namespace for room filtering. Rooms are tagged game:<gameName> automatically. Defaults to "defaultGame".
const net = new NetClient("wss://gamebackend-dk2p.onrender.com", "demoGame");

Instance Properties

PropertyTypeDescription
net.playerIdnumber | nullYour assigned player ID, set after assignedId fires.
net.roomIdstring | nullThe current room ID you are in, or null.
net.ownerIdnumber | nullThe player ID of the current room host.
net.isHostbooleanTrue if you are currently the room host.

Connection

CALL net.connect()

Opens the WebSocket connection to the server. The connected event fires when the socket is open. The server will immediately send an assignedId message with your unique player ID.

CALL net.disconnect()

Closes the WebSocket connection. The disconnected event fires. If you are in a room, the server handles cleanup and host reassignment automatically.

Rooms

CALL net.createRoom(tags?, maxClients?, isPrivate?, metaData?)

Creates a new room and makes you the host. Fires roomCreated on success.

ParameterTypeDescription
tagsstring[]Optional extra tags on the room. The game:<gameName> tag is always added automatically.
maxClientsnumberMaximum number of players including the host. Default: 8.
isPrivatebooleanIf true, adds the "private" tag and hides the room from listRooms().
metaDataobjectArbitrary JSON metadata, e.g. { name: "Bob's Lobby" }.
net.createRoom([], 8, false, { name: "My Lobby" });
CALL net.joinRoom(roomId)

Joins an existing room by its ID. Fires roomJoined on success, or error if the room doesn't exist, is full, or is closed.

ParameterTypeDescription
roomIdstringThe 5-character room code returned by roomCreated.
net.joinRoom("AB3XY");
CALL net.leaveRoom()

Leaves the current room. Fires leftRoom on this client. If you are the host, the server automatically promotes the next player and fires makeHost on their client.

CALL net.listRooms(tags?)

Requests a list of public, non-closed rooms. Filtered by the instance's gameName automatically. Fires roomList with results.

ParameterTypeDescription
tagsstring[]Optional additional tags to filter by.
net.listRooms();

net.on("roomList", (rooms) => {
  rooms.forEach(r => console.log(r.roomId, r.playerCount, r.metaData));
});

Messaging

CALL net.sendRelay(payload)

Broadcasts a JSON payload to every other player in the room. Fires relay on all recipients.

ParameterTypeDescription
payloadobjectAny JSON-serialisable object.
net.sendRelay({ x: player.x, y: player.y, anim: "run" });
CALL net.tellOwner(payload)

Sends a private JSON payload to the room host only. Useful for authoritative server-style patterns where the host validates actions. Fires tellOwner on the host.

net.tellOwner({ action: "shoot", targetId: 4521 });
CALL net.tellPlayer(playerId, payload)

Sends a private JSON payload to one specific player in the room. Fires tellPlayer on the target.

ParameterTypeDescription
playerIdnumberThe numeric ID of the target player.
payloadobjectAny JSON-serialisable object.
net.tellPlayer(98234, { msg: "You were hit!" });
CALL net.sendBinary(buffer)

Sends a raw ArrayBuffer to all players in the room. The server prepends your player ID as a uint32 before relaying. Fires binary on recipients. Use this for high-frequency data like position updates where JSON overhead matters.

const buf = new ArrayBuffer(8);
const dv  = new DataView(buf);
dv.setFloat32(0, player.x);
dv.setFloat32(4, player.y);
net.sendBinary(buf);

// Receiving binary:
net.on("binary", (fromId, buffer) => {
  const dv = new DataView(buffer);
  const x  = dv.getFloat32(0);
  const y  = dv.getFloat32(4);
});

Events

Register listeners with net.on(eventName, callback). Multiple listeners per event are supported.

Connection Events

EventCallback ArgsDescription
connectednoneWebSocket opened successfully.
disconnectednoneWebSocket closed or lost.
assignedId(playerId: number)Server has assigned you a unique numeric ID.
error(message: string)Server sent an error, e.g. room full or not found.

Room Events

EventCallback ArgsDescription
roomCreated(roomId, playerId, metaData)You successfully created a room. Share roomId with others.
roomJoined(roomId, playerId, ownerId, maxClients, metaData)You successfully joined a room.
leftRoom(roomId)You have left the room.
playerJoined(playerId)Another player joined your room.
playerLeft(playerId)Another player left or disconnected.
roomList(rooms: array)Response to listRooms(). Each room has roomId, ownerId, playerCount, maxClients, tags, metaData.

Host Events

EventCallback ArgsDescription
makeHost(oldHostId)Fired on you when you have been promoted to host (previous host left).
reassignedHost(newHostId, oldHostId)Fired on all non-host players when the host changes.
roomUpdated(metaData)Host called updateMeta() — new metadata for the room.
roomTagAdded(tag, tags)Host added a tag. tags is the full updated list.
roomTagRemoved(tag, tags)Host removed a tag.

Message Events

EventCallback ArgsDescription
relay(fromId, payload)Another player called sendRelay().
tellOwner(fromId, payload)A player called tellOwner() — only fires on the host.
tellPlayer(fromId, payload)A player called tellPlayer() targeting you.
binary(fromId, buffer: ArrayBuffer)A player sent a binary payload via sendBinary().

Host Controls

These methods only take effect if you are the current room host (net.isHost === true). The server silently ignores them otherwise.

HOST net.updateMeta(metaData)

Merges new key/value pairs into the room's metadata object. Fires roomUpdated for all players in the room.

net.updateMeta({ name: "Round 2", map: "forest" });
HOST net.addTag(tag)

Adds a string tag to the room. Fires roomTagAdded for all players. Common use: adding "closed" to prevent new joins once a game starts.

// Lock the room when the game starts
net.addTag("closed");
HOST net.removeTag(tag)

Removes a tag from the room. Fires roomTagRemoved for all players. Cannot remove game:* tags.

// Re-open the room between rounds
net.removeTag("closed");

Server Setup

The companion server.js is a Node.js WebSocket server using the ws package. It handles all room logic, host reassignment, and message routing.

  1. Install dependencies
    npm install ws
  2. Start the server locally
    node server.js
    # WebSocket server listening on port 8080
  3. Deploy to Render (free) Push server.js and a package.json to a GitHub repo. Create a new Web Service on Render, set the start command to node server.js, and deploy. Your WebSocket URL will be wss://your-service.onrender.com.
    Cold starts Render's free tier spins down after 15 minutes of inactivity. The first connection after idle may take 30–60 seconds to wake the server.

Environment Variables

VariableDefaultDescription
PORT8080Port the WebSocket server listens on. Render sets this automatically.

Server Behaviour

Heartbeat / Timeout

The server pings all connected clients every 30 seconds. Clients that do not respond with a pong are terminated and cleaned up from any room they are in.

Host Reassignment

When the host disconnects, the server promotes the first client in the room's client list to host. The new host receives makeHost. All other clients receive reassignedHost. If no clients remain, the room is deleted.

Room Capacity

The maxClients limit includes the host. A room with maxClients: 4 supports 1 host + 3 clients.

Binary Protocol

When the server receives a binary message from a client, it prepends the sender's player ID as a 4-byte unsigned int (big-endian), then forwards the message to all other clients in the room. The binary event on recipients provides both the fromId and the raw ArrayBuffer (without the prepended ID).

Example: Basic Room

A minimal host/join flow with a shared room code.

lobby.js
import NetClient from "./netClient.js";

const net = new NetClient("wss://your-server.onrender.com", "myGame");
net.connect();

// ── Create room ──────────────────────────────
document.getElementById("createBtn").onclick = () => {
  net.createRoom([], 8, false, { name: "My Game" });
};

net.on("roomCreated", (roomId) => {
  document.getElementById("roomCode").textContent = roomId;
  // Share roomId with friends so they can join
});

// ── Join room ─────────────────────────────────
document.getElementById("joinBtn").onclick = () => {
  const code = document.getElementById("codeInput").value.trim();
  net.joinRoom(code);
};

net.on("roomJoined", (roomId, playerId, ownerId, maxClients) => {
  console.log(`Joined room ${roomId}. Host is ${ownerId}`);
});

// ── Player events ─────────────────────────────
net.on("playerJoined", (id) => console.log("Player joined:", id));
net.on("playerLeft",  (id) => console.log("Player left:",  id));
net.on("error",        (msg) => console.warn("Error:", msg));

Example: Relaying State

Broadcast your player's position and colour every frame, and apply incoming updates from other players.

sync.js
const players = {}; // id → { x, y, color }
let myId = null;

net.on("assignedId", (id) => { myId = id; });

// When we or others join, seed initial state
net.on("roomJoined", () => {
  players[myId] = { x: 100, y: 100, color: randomColor() };
  net.sendRelay(players[myId]);
});

net.on("playerJoined", () => {
  // Announce yourself to the new arrival
  net.sendRelay(players[myId]);
});

net.on("playerLeft", (id) => { delete players[id]; });

// Apply remote state
net.on("relay", (fromId, data) => {
  if (!players[fromId]) players[fromId] = {};
  Object.assign(players[fromId], data);
});

// Game loop — broadcast our position every frame
function loop() {
  const me = players[myId];
  if (me) {
    handleInput(me);
    net.sendRelay({ x: me.x, y: me.y });
  }
  renderAll(players);
  requestAnimationFrame(loop);
}
loop();

Example: Full Game Loop

A complete pattern with room locking, host authority, and clean teardown — modelled after the included demo.

game.js
import NetClient from "./netClient.js";

const net = new NetClient("wss://your-server.onrender.com", "shooter");
const players = {};
let myId = null, gameRunning = false;

net.connect();
net.on("assignedId", (id) => (myId = id));

// ── Lobby ────────────────────────────────────
net.on("roomCreated", (roomId) => {
  showLobby(roomId);
});

// ── Game start (host only) ───────────────────
function startGame() {
  if (!net.isHost) return;
  net.addTag("closed");          // No new joins
  net.sendRelay({ type: "start" }); // Tell all players
  initGame();
}

net.on("relay", (fromId, data) => {
  if (data.type === "start") { initGame(); return; }

  // Position updates
  if (!players[fromId]) players[fromId] = {};
  Object.assign(players[fromId], data);
});

// ── Host actions (authoritative) ─────────────
net.on("tellOwner", (fromId, data) => {
  if (!net.isHost) return;
  if (data.action === "shoot") {
    // Validate and broadcast result
    net.sendRelay({ type: "hit", targetId: data.targetId, by: fromId });
  }
});

// ── Host reassignment ─────────────────────────
net.on("makeHost", () => {
  console.log("You are now the host");
  // Take over authority
});

// ── Teardown ─────────────────────────────────
net.on("playerLeft", (id) => { delete players[id]; });

window.addEventListener("beforeunload", () => net.disconnect());
Pattern: Host as authority Use tellOwner() for actions that need validation (shooting, scoring) and sendRelay() for frequent state that doesn't need checking (position, animation). This keeps gameplay fair without a dedicated game server.