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

playsocketjs

v5.2.1

Published

WebSocket wrapper for creating simple multiplayer systems with ease.

Readme

PlaySocket

An optimistic-first WebSocket synchronization library. Built for creating multiplayer games & collaborative experiences with reactive web frameworks.

For an introduction to PlaySocket, visit the PlaySocket website.

Why use PlaySocket?

PlaySocket makes developing shared experiences a breeze:

  • Optimistic-first: Updates apply locally right away and merge conflict-free across clients. Calling updateStorage() triggers the storageUpdated event instantly, without waiting for a server roundtrip.
  • Fast prototyping: No backend code beyond init is required, though server-authoritative validation and behavior are supported for production apps.
  • Built for reactivity: Assign the synced storage to a reactive variable via a callback, ideal for React, Svelte & co.
  • Resilient & secure: Automatic reconnection handling & strict rate-limiting.
  • Lightweight: Uses WebSockets, has few dependencies, and utilizes MessagePack for maximum efficiency.

Installation

Install PlaySocket with your package manager of choice to get started.

npm install playsocketjs

PlaySocket Client

The client-side part of PlaySocket.

Examples

[!NOTE] In production, you should always try...catch promises such as socket.init() as they can reject. These examples omit the error handling to keep them simple.

Initializing the client:

import PlaySocket from "playsocketjs";

// Create a new instance
// Omit the ID to let the server pick one
const socket = new PlaySocket("unique-client-id", {
    endpoint: "wss://example.com/socket"
});

// Set up the event handlers you need
socket.onEvent("status", status => console.log(status));
socket.onEvent("storageUpdated", storage => { /* ... */ });

const clientId = await socket.init(); // Connect

Creating a room:

// Create a new room
const roomId = await socket.createRoom();
// Optionally, with initial storage
const roomId = await socket.createRoom({
  players: [{ name: "Player-1", level: 35 }],
  latestPlayer: null,
});

Joining a room:

await socket.joinRoom("room-id"); // Join an existing room

Leaving a room:

socket.destroy(); // Destroy the instance to leave

Using the storage update event with reactivity:

// Assign to useState(), $state(), reactive() etc. on update
const [reactiveVar, setReactiveVar] = useState({});
socket.onEvent("storageUpdated", storage => {
    setReactiveVar(storage);
});
<p>Players: {reactiveVar.players?.join(", ")}</p>

Interfacing with the synchronized storage:

const currentState = socket.storage; // Read-only access

socket.updateStorage("players", "array-add-unique", { username: "Player4", level: 2 });
socket.updateStorage("latestPlayer", "set", "Player4");
socket.updateStorage("playerInfo", "set", { date: "22-6-2026" });
socket.updateStorage("playerInfo", "object-set-key", "color", "red")

console.log(socket.storage.players); // Log players array

Sometimes it's convenient to send a traditional request to the server. For example, when you want to opt out of optimistic updates for asynchronous work, or when the validation logic would be too complex otherwise:

await socket.sendRequest("my-request-name", { fact: "You can build traditional client-server logic like this." })

API

Constructor

Create a new PlaySocket instance with a specified ID and configuration options. The ID can be set to null to let the server pick a unique one.

new PlaySocket(id?: string, options: PlaySocketOptions)

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | endpoint | string | Yes | undefined | WebSocket server endpoint (e.g., wss://example.com/socket). | | customData | object | No | {} | Arbitrary data to pass to the "clientRegistered" server event. | | debug | boolean | No | false | Set to true to enable extra logging. |

Methods

| Name | Parameters | Return type | Description | |--------|------------|-------------|-------------| | init() | - | Promise<string> | Initialize the WebSocket connection, resolves with the client ID. | | createRoom() | initialStorage?: object, size?: number | Promise<string> | Create a new room, resolves with the room ID. Max. 500 participants. | | joinRoom() | roomId: string | Promise<void> | Join an existing room. | | destroy() | reason?: string | void | Leave room, close the connection, and destroy the instance. | | updateStorage() | key: string, type: string, value: any, secondValue?: any | void | Update a key in the shared storage. | | sendRequest() | name: string, data?: any | Promise<void> | Send a request to the server with optional attached data. | | onEvent() | event: string, callback: Function | () => void | Register an event callback. Returns unsubscribe function. |

Events

| Event | Callback parameter | Description | |-------|-------------------|-------------| | status | status: string | Connection or room status changed, e.g. display in UI during joining or room creation. | | moved | roomId: string | Moved to different room. | | instanceDestroyed | reason: string | Instance destroyed through destroy() or error. | | storageUpdated | storage: object | Storage state changed. Does not trigger on no-op updates. | | hostMigrated | hostId: string | Host was changed. | | clientJoined | clientId: string | New client joined the room. | | clientLeft | clientId: string | A client left the room. |

Properties

| Property | Type | Description | |----------|------|-------------| | id | string | Client's unique ID. | | isHost | boolean | Whether this client is currently assigned the host role. | | participantCount | number | Number of active client connections in room. | | storage | object | Retrieve the storage object. |

PlaySocket Server

The server-side part of PlaySocket.

Examples

Using PlaySocket as a standalone server:

import PlaySocketServer from "playsocketjs/server";

// Path defaults to "/"
const server = new PlaySocketServer();

function shutdown() {
    server.stop(); // Gracefully disconnect all clients
    process.exit(0);
}

process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);

Using PlaySocket together with Express.js (or other backend frameworks):

import express from "express";
import http from "node:http";
import PlaySocketServer from "playsocketjs/server";

const app = express();
const httpServer = http.createServer(app);

// Create PlaySocket server with existing HTTP server
const playSocketServer = new PlaySocketServer({
  server: httpServer,
  path: "/socket"
});

// Start the server
httpServer.listen(3000, () => {
  console.log("Server running on port 3000.");
});

function shutdown() {
    playSocketServer.stop(); // Gracefully disconnect clients
    process.exit(0);
}

process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);

Validating an incoming storage update:

const server = new PlaySocketServer();

server.onEvent("storageUpdateRequested", ({ roomId, clientId, update, storage }) => {
    const { key, type, value, secondValue } = server.getUpdateDetails(update);

    // Block updates on all keys except for "players" and "chats"
    if (!["players", "chats"].includes(key)) return false;

    if (key === "chats") {
        if (type !== "array-add") return false; // Only allow adding chats
        if (typeof value !== "string") return false; // Only allow strings
    }
});

Handling an incoming request:

const server = new PlaySocketServer();

server.onEvent("requestReceived", async ({ roomId, clientId, name, data }) => {
    if (name === "add-player-request") {
        const players = server.getRoomStorage(roomId)?.players || [];

        // Returning false or string rejects sendRequest() on the client
        if (players.find(p => p.id === clientId)) return "Player already added";

        server.updateRoomStorage(roomId, "players", "array-add", { id: clientId, timestamp: Date.now() });
    }
});

Combining PlaySocket with a custom authentication system:

const authedClients = [];
const server = new PlaySocketServer();

server.onEvent("clientRegistrationRequested", async (clientId, data) => {
    try {
        // Your custom auth logic...
        // For example, data could contain a token
        authedClients.push(clientId);
    } catch (error) {
        return "Error occurred during auth"; // Blocks the registration
    }
});

server.onEvent("clientDisconnected", async (clientId) => {
    const removeIndex = authedClients.indexOf(clientId);
    if (removeIndex !== -1) authedClients.splice(removeIndex, 1);
});

API

Constructor

Create a new PlaySocket Server instance with configuration options.

new PlaySocketServer(options?: PlaySocketServerOptions)

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | port | number | No | 3000 | Port to listen on (used only if no server provided). | | path | string | No | "/" | WebSocket endpoint path. | | server | http.Server | No | - | Existing http server. | | rateLimit | number | No | 20 | Messages/second rate limit. | | debug | boolean | No | false | Enable debug logging. | | verifyClient | function | No | - | Callback to verify connections before WebSocket upgrade. |

The verifyClient option allows you to implement custom connection verification logic, such as rate limiting, before the WebSocket handshake completes.

const server = new PlaySocketServer({
    server: httpServer,
    path: "/socket",
    verifyClient: (info, callback) => {
        const forwarded = info.req.headers["x-forwarded-for"];
        const ip = forwarded ? forwarded.split(",")[0].trim() : info.req.socket.remoteAddress;
        if (isRateLimited(ip)) {
            return callback(false, 429, "Too Many Requests");
        }
        callback(true);
    }
});

The callback signature is callback(verified, code?, message?) where code refers to an HTTP status code and message to a rejection reason.

Methods

[!IMPORTANT] Rooms created by the server are always owned by the host ID "server". Such rooms are not auto-deleted when all participants have left, so you need to take care of their lifecycle.

| Name | Parameters | Return type | Description | |--------|------------|-------------|-------------| | stop() | - | void | Closes active client connections and the underlying http server (if it's standalone). | | kick() | clientId: string, reason?: string | void | Kick a client by their client ID. | | move() | clientId: string, roomId: string | void | Move a client that is already in a room to a different room. | | onEvent() | event: string, callback: Function | () => void | Register a server-side event callback. Returns unsubscribe function. | | getRoomStorage() | roomId: string, options?: { dangerouslySkipCloning?: boolean } | object | Get a snapshot of the current room storage. Pass dangerouslySkipCloning: true to get the uncloned store for performance reasons. | | getUpdateDetails() | update: object | object | Get the details (key, type, value and secondValue) of a storage update for building validation logic. | | updateRoomStorage() | roomId: string, key: string, type: string, value: any, secondValue?: any | void | Update a key in the shared storage of a room. | | createRoom() | initialStorage?: object, size?: number | object | Create a server-owned room (returns object containing room ID and state).| | destroyRoom() | roomId: string | void | Destroy a room & kick all participants. |

Events

| Event | Callback parameters | Description | Return for action | |-------|-------------------|-------------|--------------| | clientRegistered | clientId: string, customData: object | Client registered with the server. | - | | clientRegistrationRequested | clientId: string, customData: object | Client requested to register. | Return false or rejection reason string to block. | | clientDisconnected | clientId: string | Client disconnected. | - | | clientJoinedRoom | clientId: string, roomId: string | Client joined a room. | - | | clientJoinRequested | clientId: string, roomId: string | Client requested to join a room. | Return false or rejection reason string to block. | | clientLeftRoom | clientId: string, roomId: string | Client left a room. | - | | roomCreated | roomId: string | Room was created. | - | | roomDestroyed | roomId: string | Room was destroyed. | - | | roomCreationRequested | {clientId: string, initialStorage: object} | Client requested to create room. | Return object to override initial storage, false or rejection reason string to block. | | storageUpdated | {clientId: string, roomId: string, update: object, storage: object} | Room storage updated. | - | | storageUpdateRequested | {clientId: string, roomId: string, update: object, storage: object} | Client requested storage update. | Return false or rejection reason string to block the update. Callback must be synchronous. | | requestReceived | {clientId: string, roomId?: string, name: string, data?: any} | Request from client. | Return false or rejection reason string to block. |

When multiple callbacks are registered for an event, all of them run, and for events that respond to a return value, the first non-null return value is used. A callback that throws counts as returning false.

Properties

| Property | Type | Description | |----------|------|-------------| | rooms | object | Retrieve the rooms object. |

Storage

Both updateStorage() and updateRoomStorage() work the same way. The only difference is that the latter takes roomId as the first argument and runs on the server. There's a limit of 100 storage keys.

Number, array and object operation types allow for conflict-free simultaneous updates. The set operation just replaces the property and ensures correct ordering.

For -matching operation types, value becomes the value to match, and secondValue the replacement. For object operations, value is the property key, and secondValue the property value.

Operation types

  • set
  • number-increment
  • array-add
  • array-add-unique
  • array-update-matching
  • array-remove-matching
  • object-set-key
  • object-remove-key

Example for each type:

  • updateStorage("color", "set", "blue")
  • updateStorage("score", "number-increment", 25)
  • updateStorage("players", "array-add", { name: "Player2" })
  • updateStorage("completedLevels", "array-add-unique", 14)
  • updateStorage("names", "array-update-matching", "Leo_cool", "TheCoolerLeo")
  • updateStorage("missingLevels", "array-remove-matching", 14)
  • updateStorage("levelNames", "object-set-key", "evilSea", "Evil sea")
  • updateStorage("levelNames", "object-remove-key", "darkOcean")

License

MIT