npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

game-networking-js

v1.0.5

Published

A library for handling real-time multiplayer game state.

Downloads

4

Readme

game-networking-js

Introduction

game-networking-js is a client-side JavaScript library designed to simplify the addition of real-time, peer-to-peer (P2P) multiplayer functionality to web-based games. It leverages PeerJS for WebRTC connections and Supabase for robust, serverless matchmaking.

This library abstracts away the complexities of WebRTC and room management, providing a high-level, event-driven API so you can focus on your game's logic.

Key features include:

  • Hosting game sessions with automatic room creation and cleanup.
  • Finding and joining random games via a matchmaking queue.
  • Joining specific games using a unique room ID or a shareable link/QR code.
  • An event-driven API for handling player connections, disconnections, and data messages.
  • A clear, host-authoritative architectural pattern to prevent common P2P synchronization issues.

Architectural Model: Host-Authoritative

This library enforces a host-authoritative networking model. This is crucial for preventing desynchronization bugs.

  • The Host is the Source of Truth: One player (the "host") is responsible for managing the definitive game state. The host runs the full game logic, processes all player inputs, and resolves all outcomes.
  • Clients are "Dumb" Renderers: All other players (the "clients") do not run their own game logic. They send their inputs to the host and then simply render the updated game state they receive back from the host.

Adhering to this model is the key to building a stable multiplayer experience with this library.

Prerequisites

Before using the library, your HTML page must include the PeerJS and Supabase client libraries.

<script src="https://unpkg.com/[email protected]/dist/peerjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>

Installation

The recommended way to use the library is by importing it as an ES6 module directly in your project.

// main.js
import * as GameNetworking from './src/index.js';

Service Worker Configuration (IMPORTANT)

If your application uses a Service Worker (sw.js) for caching and offline capabilities, you must add all the library's source files to your cache list. Forgetting this step will cause the library to fail to load on subsequent visits, leading to application-breaking errors.

Example sw.js urlsToCache array:

const CACHE_NAME = 'your-game-cache-v1';
const urlsToCache = [
  // ... your other app files (index.html, style.css, main.js)
  
  // Add all game-networking-js library files
  './src/index.js',
  './src/core/GameNetworking.js',
  './src/core/PeerManager.js',
  './src/core/StateManager.js',
  './src/core/SupabaseManager.js',
  './src/utils/ErrorCodes.js',
  './src/utils/MessageTypes.js'
];

Note: Always remember to increment your CACHE_NAME version whenever you update the urlsToCache list to ensure the service worker updates correctly.

Supabase Setup

1. Create the Matchmaking Table

In your Supabase project's SQL editor, create the game_rooms table. This table will be used to list available game sessions for matchmaking.

CREATE TABLE game_rooms (
  room_id TEXT PRIMARY KEY,
  host_peer_id TEXT NOT NULL,
  game_type_identifier TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'waiting', -- 'waiting', 'full', 'in_game'
  max_players INT NOT NULL DEFAULT 2,
  current_players INT NOT NULL DEFAULT 1,
  game_settings JSONB,
  expires_at TIMESTAMPTZ NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Create an index for faster room queries
CREATE INDEX idx_available_game_rooms ON game_rooms (
  game_type_identifier, 
  status, 
  expires_at, 
  current_players, 
  max_players
);

2. Create the Required RPC Function

This database function is essential for the findRandomGame feature. It efficiently finds an open room that matches the game criteria.

CREATE OR REPLACE FUNCTION find_available_room(
  p_game_type_identifier TEXT,
  p_game_settings_filter JSONB DEFAULT NULL
)
RETURNS TABLE(
  room_id TEXT,
  host_peer_id TEXT,
  game_type_identifier TEXT,
  status TEXT,
  max_players INT,
  current_players INT,
  game_settings JSONB,
  expires_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ
) AS $$
BEGIN
  RETURN QUERY
  SELECT 
    gr.room_id,
    gr.host_peer_id,
    gr.game_type_identifier,
    gr.status,
    gr.max_players,
    gr.current_players,
    gr.game_settings,
    gr.expires_at,
    gr.created_at
  FROM game_rooms gr
  WHERE gr.game_type_identifier = p_game_type_identifier
    AND gr.status = 'waiting'
    AND gr.current_players < gr.max_players
    AND gr.expires_at > now()
    AND (
        p_game_settings_filter IS NULL 
        OR gr.game_settings @> p_game_settings_filter
      )
  ORDER BY gr.created_at ASC
  LIMIT 1;
END;
$$ LANGUAGE plpgsql;

3. Set Up Row Level Security (RLS)

Enable RLS and create policies to allow public access for creating, finding, and updating rooms.

-- 1. Enable RLS on the table
ALTER TABLE game_rooms ENABLE ROW LEVEL SECURITY;

-- 2. Create policies for public access
CREATE POLICY "Allow public read access" ON game_rooms FOR SELECT USING (true);
CREATE POLICY "Allow public insert" ON game_rooms FOR INSERT WITH CHECK (true);
CREATE POLICY "Allow public update" ON game_rooms FOR UPDATE USING (true);
CREATE POLICY "Allow public delete" ON game_rooms FOR DELETE USING (true);

4. Optional: Auto-Update updated_at Timestamp

This trigger automatically updates the updated_at column whenever a room's details change.

CREATE OR REPLACE FUNCTION handle_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = now();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER on_game_rooms_update
BEFORE UPDATE ON game_rooms
FOR EACH ROW
EXECUTE PROCEDURE handle_updated_at();

Quick Start

1. Initialize the Library

This must be done once when your application starts.

import * as GameNetworking from './src/index.js';

const config = {
  supabaseUrl: 'YOUR_SUPABASE_URL',
  supabaseKey: 'YOUR_SUPABASE_ANON_KEY',
  tableName: 'game_rooms', // Must match your Supabase table name
  gameTypeIdentifier: 'dots-and-boxes-v1', // A unique ID for your game
  qrCodeBaseUrl: 'https://your-game-url.com' // For QR code generation
};

// Initialize the library when your app loads
await GameNetworking.initialize(config);
console.log('Game networking is ready!');

2. Host a Game

const hostPlayerData = { name: "Alice", icon: "👑", color: "#FF6B6B" };
const gameSettings = { maxPlayers: 2, boardSize: "4x4" };

const hostCallbacks = {
  onPlayerJoined: (player) => { console.log(`${player.name} joined!`); },
  onPlayerLeft: (playerId) => { console.log(`Player ${playerId} left.`); },
  onDataReceived: (data, fromPlayerId) => {
    // Host processes client inputs here
    handleClientMove(fromPlayerId, data);
  },
  onError: (error) => { console.error('Host error:', error); }
};

try {
  // hostGame returns the host's complete player object
  const { roomId, qrCodeData, hostPlayer } = await GameNetworking.hostGame(
    hostPlayerData, 
    gameSettings, 
    hostCallbacks
  );
  
  console.log('Room created! ID:', roomId);
  console.log('Host player object:', hostPlayer);
  
  // Now, display the QR code or link for others to join
  displayQRCode(qrCodeData);
  
} catch (error) {
  console.error('Failed to host game:', error);
}

3. Join a Game

const clientPlayerData = { name: "Bob", icon: "🚀", color: "#4ECDC4" };

const clientCallbacks = {
  onSearching: () => { console.log('Looking for a game...'); },
  onGameFound: (room) => { console.log('Game found! Connecting...'); },
  onJoinSuccess: (details) => {
    console.log('Successfully joined room!', details);
    // details.yourData contains your final player info from the host
  },
  onJoinFailed: (error) => { console.error('Could not join:', error.message); },
  onDataReceived: (data, hostPeerId) => {
    // Client renders authoritative state from host
    handleHostUpdate(data);
  },
  onHostLeft: () => { console.log('The host has left the game.'); },
  onError: (error) => { console.error('Client error:', error); }
};

// To join a random game
const gameFilter = { boardSize: "4x4" }; // Optional filter
GameNetworking.findRandomGame(clientPlayerData, gameFilter, clientCallbacks);

// To join a specific game by ID (e.g., from a URL)
const roomId = "dots-and-boxes-v1-some-peer-id"; 
GameNetworking.joinGameById(roomId, clientPlayerData, clientCallbacks);

State Synchronization Guide

To prevent bugs, the host must be the single source of truth. Here is a recommended implementation pattern based on a dots-and-boxes game.

Host-Side Logic (hostCallbacks.onDataReceived)

When a client sends a move, the host validates it, processes it, and broadcasts the complete result to all players.

// A simplified example of the host's move handler
function handleClientMove(fromPlayerId, data) {
  if (data.type !== 'SUBMIT_MOVE') return;

  // 1. Validate the move (e.g., is it this player's turn?)
  if (state.currentPlayerId !== fromPlayerId) {
    return; // Ignore out-of-turn move
  }

  // 2. Process the move and get all outcomes
  //    processMove should return the coordinates of any completed boxes
  const completedBoxes = gameLogic.processMove(data.move, fromPlayerId);

  // 3. Broadcast the authoritative result to ALL clients
  GameNetworking.broadcastData({
    type: 'GAME_STATE_UPDATE',
    lastMove: { ...data.move, playerIndex: fromPlayerId },
    completedBoxes: completedBoxes, // Array of {r, c, owner}
    nextPlayerId: state.currentPlayerId, // The new current player
    updatedScores: state.playerScores // The new scores
  });

  // 4. If game is over, broadcast that too
  if (isGameOver()) {
    GameNetworking.broadcastData({ type: 'GAME_OVER', ... });
  }
}

Client-Side Logic (clientCallbacks.onDataReceived)

The client does not run game logic. It only receives GAME_STATE_UPDATE messages and renders the result.

// A simplified example of the client's update handler
function handleHostUpdate(data) {
  if (data.type !== 'GAME_STATE_UPDATE') return;

  // 1. Render the line that was drawn
  ui.drawVisualLine(data.lastMove);

  // 2. Render all completed boxes sent by the host
  data.completedBoxes.forEach(box => {
    ui.fillBoxOnBoard(box.r, box.c, box.owner);
  });
  
  // 3. Update scores and turn display with data from the host
  ui.updateScores(data.updatedScores);
  ui.updatePlayerTurn(data.nextPlayerId);
}

API Reference

Core Functions

  • initialize(config): Initializes the library. Must be called first.
  • hostGame(hostPlayerData, gameSettings, callbacks): Creates a new game session. Returns a Promise that resolves with { roomId, qrCodeData, hostPlayer }.
  • findRandomGame(playerData, filter, callbacks): Finds and joins an available game.
  • joinGameById(roomId, playerData, callbacks): Joins a game by its specific ID.
  • leaveGame(): Disconnects from the current session and cleans up resources.
  • sendDataToHost(data): (Client-only) Sends a JSON-serializable object to the host.
  • sendDataToPlayer(playerId, data): (Host-only) Sends data to a specific client by their room-local ID.
  • broadcastData(data, excludePeerId?): (Host-only) Sends data to all connected clients.
  • getLocalPeerId(): Returns the local client's raw PeerJS ID, or null.

MSG_TYPE

The library exports an enum-like object, MSG_TYPE, containing standardized message types for internal and external communication. It is highly recommended to use these for your own game-specific messages to avoid conflicts. import { MSG_TYPE } from './src/index.js';

Callbacks

Provide hostCallbacks or clientCallbacks objects to the networking functions. These objects contain functions that are triggered on specific events.

Host Callbacks (hostCallbacks)

  • onPlayerAttemptingJoin: (data, accept, reject) => {}: (Optional) Intercept join requests to approve or deny them.
  • onPlayerJoined: (player) => {}
  • onPlayerLeft: (playerId) => {}
  • onDataReceived: (data, fromPlayerId) => {}
  • onError: (error) => {}

Client Callbacks (clientCallbacks)

  • onSearching: () => {}
  • onGameFound: (roomDetails) => {}
  • onNoGameFound: () => {}
  • onJoinSuccess: (details) => {}
  • onJoinFailed: (error) => {}
  • onDataReceived: (data, fromHostPeerId) => {}
  • onHostLeft: () => {}
  • onError: (error) => {}

Troubleshooting

  1. "Cannot read properties of undefined..." on init: Ensure sw.js is caching all files from the /src directory and that you have cleared your browser cache after updating the service worker.
  2. Players can't connect: This is often a NAT or firewall issue. Using a TURN server can significantly improve connection success rates. You can add TURN server credentials to the peerJsConfig object during initialization.
  3. Game state desynchronizes: You are likely running game logic on the client. Refactor your code to follow the host-authoritative model strictly. The client should only send its input to the host and render the state it receives back.