NetClient
A lightweight WebSocket networking library for browser games. Rooms, relays, host management, tags, metadata, and binary support — all in one small file.
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.
-
Download netClient.js Copy
netClient.jsinto your project folder alongside your HTML file. -
Import it as an ES moduleyour-game.js
import NetClient from "./netClient.js";Note Your script tag must usetype="module"for ES module imports to work. -
Add
type="module"to your script tagindex.html<script type="module" src="./your-game.js"></script> -
Point to a server NetClient connects to any WebSocket server that speaks its protocol. You can self-host
server.json 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. Deployserver.jswithnode server.js.
Quick Start
Get two players connected and talking in under 20 lines.
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
Creates a new NetClient instance. Does not connect until connect() is called.
| Parameter | Type | Description |
|---|---|---|
| url | string | WebSocket server URL, e.g. "wss://your-server.onrender.com" |
| gameName | string | Optional. 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
| Property | Type | Description |
|---|---|---|
| net.playerId | number | null | Your assigned player ID, set after assignedId fires. |
| net.roomId | string | null | The current room ID you are in, or null. |
| net.ownerId | number | null | The player ID of the current room host. |
| net.isHost | boolean | True if you are currently the room host. |
Connection
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.
Closes the WebSocket connection. The disconnected event fires. If you are in a room, the server handles cleanup and host reassignment automatically.
Rooms
Creates a new room and makes you the host. Fires roomCreated on success.
| Parameter | Type | Description |
|---|---|---|
| tags | string[] | Optional extra tags on the room. The game:<gameName> tag is always added automatically. |
| maxClients | number | Maximum number of players including the host. Default: 8. |
| isPrivate | boolean | If true, adds the "private" tag and hides the room from listRooms(). |
| metaData | object | Arbitrary JSON metadata, e.g. { name: "Bob's Lobby" }. |
net.createRoom([], 8, false, { name: "My Lobby" });
Joins an existing room by its ID. Fires roomJoined on success, or error if the room doesn't exist, is full, or is closed.
| Parameter | Type | Description |
|---|---|---|
| roomId | string | The 5-character room code returned by roomCreated. |
net.joinRoom("AB3XY");
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.
Requests a list of public, non-closed rooms. Filtered by the instance's gameName automatically. Fires roomList with results.
| Parameter | Type | Description |
|---|---|---|
| tags | string[] | Optional additional tags to filter by. |
net.listRooms();
net.on("roomList", (rooms) => {
rooms.forEach(r => console.log(r.roomId, r.playerCount, r.metaData));
});
Messaging
Broadcasts a JSON payload to every other player in the room. Fires relay on all recipients.
| Parameter | Type | Description |
|---|---|---|
| payload | object | Any JSON-serialisable object. |
net.sendRelay({ x: player.x, y: player.y, anim: "run" });
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 });
Sends a private JSON payload to one specific player in the room. Fires tellPlayer on the target.
| Parameter | Type | Description |
|---|---|---|
| playerId | number | The numeric ID of the target player. |
| payload | object | Any JSON-serialisable object. |
net.tellPlayer(98234, { msg: "You were hit!" });
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
| Event | Callback Args | Description |
|---|---|---|
| connected | none | WebSocket opened successfully. |
| disconnected | none | WebSocket 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
| Event | Callback Args | Description |
|---|---|---|
| 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
| Event | Callback Args | Description |
|---|---|---|
| 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
| Event | Callback Args | Description |
|---|---|---|
| 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.
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" });
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");
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.
-
Install dependencies
npm install ws -
Start the server locally
node server.js # WebSocket server listening on port 8080 -
Deploy to Render (free) Push
server.jsand apackage.jsonto a GitHub repo. Create a new Web Service on Render, set the start command tonode server.js, and deploy. Your WebSocket URL will bewss://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
| Variable | Default | Description |
|---|---|---|
| PORT | 8080 | Port 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.
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.
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.
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());
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.