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

@playmesh/client

v0.6.0

Published

PlayMesh client SDK — connect games and applications to PlayMesh servers.

Readme

@playmesh/client

PlayMesh client SDK — connect games and applications to PlayMesh servers.

npm License: MIT

Website · GitHub · npm · Example

Overview

@playmesh/client is the client-side SDK for communicating with PlayMesh servers. It provides:

  • Connection Management — Reliable WebSocket connections with automatic reconnection
  • Event Communication — Send and receive events from the server
  • Session Lifecycle — Track session info and instance membership
  • Server Messaging — Listen for targeted and broadcast messages
  • Type-Safe APIs — Full TypeScript support for client-server communication

Installation

npm install @playmesh/client

Quick Start

import { PlayMeshClient } from '@playmesh/client'

const client = new PlayMeshClient({
  url: 'https://game.example.com',
  auth: { token: 'user-jwt-token' }
})

const session = await client.connect()
console.log(session.userId, session.instances)

// Send events to the server
client.emit('player:move', { x: 100, y: 200 })

// Send an event to exactly one instance you are a member of
client.emitTo('world/town-square', 'player:move', { x: 100, y: 200 })

// Request/response against one instance
const result = await client.requestTo('shop/main', 'shop:buy', { itemId: 'sword' }, { timeout: 5_000 })

// Receive events from the server
client.on('player:update', data => {
  console.log('Player update:', data)
})

// Every listener API returns an unsubscribe function
const stop = client.onDisconnect(reason => {
  console.log('Disconnected:', reason)
})
stop()

client.onReconnect(() => {
  console.log('Reconnected')
})

The client and server must run matching PlayMesh releases: the SDK announces its wire-protocol version during the handshake, and a mismatch is rejected with a clear error instead of failing unpredictably.

Configuration

Basic Connection

const client = new PlayMeshClient({
  url: 'https://game.example.com'
})

With Authentication

const client = new PlayMeshClient({
  url: 'https://game.example.com',
  auth: {
    token: 'jwt-token-or-session-id',
    userId: 'player1'
  }
})

Dynamic Auth (Token Refresh)

Pass a function to re-evaluate auth on every connection attempt:

const client = new PlayMeshClient({
  url: 'https://game.example.com',
  auth: async () => ({ token: await authService.getToken() })
})

Socket.IO Options

Reconnection and other Socket.IO options are passed under socket:

const client = new PlayMeshClient({
  url: 'https://game.example.com',
  socket: {
    reconnection: true,
    reconnectionDelay: 1000,
    reconnectionDelayMax: 5000,
    reconnectionAttempts: Infinity
  }
})

Synced State

The client keeps live read replicas of the synced state of every instance it is in, seeded on join and updated in real time. Public state is shared by all members; your per-user private state is delivered only to you. All of it is server-written — clients read:

client.stateOf('world/city') // public state replica
client.userStateOf('world/city') // your private state (only you receive it)
client.onStateChange(change => {
  // change.scope === 'user' marks your private state
  // change.revision is the monotonic per-scope revision
})

Every change carries a monotonic revision. The replica applies changes strictly in order: duplicates and stale changes are ignored, and if a change is ever lost the client detects the gap and transparently fetches a fresh snapshot over the same connection — stateOf() / userStateOf() are always the authoritative view. (During a recovery, individual onStateChange callbacks for the skipped changes may not fire; re-read the replica instead of accumulating changes yourself.)

Presence

See who is in each of your instances and react to joins and leaves:

client.presenceOf('world/city') // { count, users }
client.onPresence(event => {
  // { instance, type: 'join' | 'leave', userId, sessionId, count }
})

Chat

Built-in chat delivers one message per instance you are a member of — a player in a world room and a minigame speaks in both with one call, and each delivery carries its instance's path. chatTo targets one room. The server moderates each delivery in real time and may rewrite it, block it, or kick you:

// Speak in every room the player currently belongs to.
client.chat('Hello everyone')

// Speak only in the minigame room.
client.chatTo('minigames/race-42', 'Ready!')

client.onChat(message => console.log(`[${message.instance}] ${message.userId}: ${message.text}`))
client.onKick(reason => console.log('Kicked:', reason))

Join Requests

Ask to join or leave instances. Joins are vetoed by the server unless the instance explicitly allows them (onJoinRequest server-side):

await client.join('world/vip-lounge') // rejects with the server's denial message
await client.leave('world/vip-lounge')

Scoped Events and Requests

emit() reaches every instance you have joined (with a matching server handler). emitTo() targets exactly one instance you are a member of, and requestTo() awaits a typed response from that instance's onRequest handler:

client.emitTo('world/town-square', 'player:move', { x: 100, y: 200 })

const result = await client.requestTo('shop/main', 'shop:buy', { itemId: 'sword' }, { timeout: 5_000 })

Requests use a unique correlation id and reject on timeout (default 10s), on disconnect, when you leave the target instance, or with the server's safe error message. Unknown and unauthorized targets produce one generic error.

Typed Events

Pass event maps to type emit/on end to end, and a request map to type requestTo (compile-time only):

type ClientEvents = { 'player:move': { x: number; y: number } }
type ServerEvents = { 'player:update': { x: number; y: number; by: string } }
type Requests = {
  'shop:buy': { request: { itemId: string }; response: { success: boolean; balance: number } }
}

const client = new PlayMeshClient<ClientEvents, ServerEvents, Requests>({ url })
client.emit('player:move', { x: 1, y: 2 }) // payload type-checked
const result = await client.requestTo('shop/main', 'shop:buy', { itemId: 'sword' }) // typed response

Message Signing

When the server enables signing, enable it on the client too — signing is optional but must match on both sides. A signing client refuses to talk to a server that does not sign (and vice versa); there is no silent downgrade to unsigned mode.

The client generates an ephemeral ECDSA P-256 keypair per connect() (the private key is created non-extractable), exchanges public keys with the server during the handshake, and signs/verifies every application event and every data-bearing protocol message (scoped events, requests and responses, chat, state changes and snapshots, join snapshots, presence) with SHA-256. Instance paths, event names and request ids ride inside the signed payload, so routing cannot be tampered with. Only playmesh:session (the key exchange itself), playmesh:error and playmesh:kicked stay unsigned — see the server README for the full model:

const client = new PlayMeshClient({
  url: 'https://game.example.com',
  signing: true
})

Each connection receives a random session nonce from the server that is signed into every envelope, and every envelope carries a strictly increasing sequence number. Together they prevent replay: within a session (sequence numbers) and across connections, reconnects and server nodes (nonces). After a reconnect the client waits for the new session nonce before sending signed events; events queued for the old connection are dropped and reported via onError rather than replayed.

Messages that fail verification are dropped and reported via onError.

Payload restrictions: signed payloads must be JSON-compatible (objects, arrays, strings, booleans, null, finite numbers). Date values serialize as ISO strings and arrive as strings. Binary values (typed arrays, ArrayBuffer, Blob, Node Buffer), BigInt, functions, symbols, non-finite numbers and circular structures make emit() throw instead of silently corrupting the message.

Security notes: signing does not replace TLS — the key/nonce exchange depends on transport security, so production must use https/wss (plain http://localhost is fine for development, and the client warns when signing is enabled over an insecure non-local URL). Signing also does not prevent cheating: a malicious client can sign arbitrary payloads with its own key, so the server must still validate and authorize everything.

API Reference

PlayMeshClient

Constructor options (PlayMeshClientOptions):

| Option | Type | Description | | --------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | url | string | Server URL, e.g. https://game.example.com or http://localhost:3000 | | auth | Record<string, unknown> \| () => Record<string, unknown> | Auth payload sent to the server's authentication hook | | socket | Partial<ManagerOptions & SocketOptions> | Options forwarded to the underlying Socket.IO client | | signing | boolean | Sign application events and data-bearing protocol messages. The server must enable signing too. |

Methods:

  • connect(): Promise<SessionInfo> — Connect and authenticate. Resolves once the server has established the session.
  • disconnect(): void — Disconnect from the server
  • emit(event, payload?): void — Send an event to the server (dispatched to every joined instance and relevant domain)
  • emitTo(instancePath, event, payload?): void — Send an event to exactly one instance you are a member of
  • requestTo(instancePath, event, payload?, { timeout? }): Promise<Response> — Request/response against one instance (default timeout 10s)
  • join(instancePath: string): Promise<void> — Ask to join an instance; the server can veto
  • leave(instancePath: string): Promise<void> — Ask to leave an instance (always honored)
  • chat(text: string): void — Send a chat message to every instance you are in (server-moderated per instance)
  • chatTo(instancePath: string, text: string): void — Send a chat message to one instance you are in
  • stateOf(instancePath) / userStateOf(instancePath) — Public / private synced-state replicas (revision-consistent)
  • presenceOf(instancePath): PresenceInfo | undefined — Live { count, users } for an instance

Listeners — every listener registration returns an Unsubscribe function (() => void, safe to call repeatedly). off(event, handler) also removes on listeners:

  • on(event, handler): Unsubscribe — Listen for an event from the server
  • off(event, handler): void — Remove a listener registered with on
  • onStateChange(handler): Unsubscribe — Called when synced public or private state changes
  • onPresence(handler): Unsubscribe — Called when a session joins or leaves an instance you are in
  • onChat(handler): Unsubscribe — Called for chat messages in your instances
  • onKick(handler): Unsubscribe — Called when the server kicks this client, just before the disconnect
  • onDisconnect(handler): Unsubscribe — Called when the connection drops
  • onReconnect(handler): Unsubscribe — Called when the connection is automatically re-established
  • onError(handler): Unsubscribe — Called when the server reports a session error

Getters:

  • session: SessionInfo | undefined — Session details, available once connect() resolves
  • instances: string[] — Current instance paths (domainId/instanceId) the session belongs to
  • connected: boolean — Whether the socket is currently connected

SessionInfo

Returned by connect() and available via client.session.

  • id — Session ID
  • userId — User ID established by the server's authentication hook
  • instances — Instance paths (domainId/instanceId) the session is a member of

ServerError

Passed to onError() handlers.

  • scope: 'connection' | 'event' | 'join' | 'chat' — Where the error occurred
  • event?: string — Event name, when scope is 'event'
  • instance?: string — The instance involved, for join errors and per-instance chat rejections
  • message: string — Error description

Examples

Multiplayer Chat

const client = new PlayMeshClient({ url: 'https://chat.example.com' })
await client.connect()

client.on('chat', data => {
  console.log(`${data.username}: ${data.text}`)
})

client.emit('send-message', { text: 'Hello everyone!' })

Game World

const client = new PlayMeshClient({ url: 'https://game.example.com' })
await client.connect()

client.on('player:update', player => {
  updatePlayerPosition(player.id, player.position)
})

client.emit('player:move', { x: mouse.x, y: mouse.y })

Building

npm run build      # Build with tsup
npm run typecheck  # Type check with TypeScript

Browser Compatibility

The client works in modern browsers (ES2020+) and Node.js 18+. It requires WebSocket support for real-time communication.

Philosophy

PlayMesh clients are simple and focused:

  • Connect to the server
  • Send events to the server
  • Receive events from the server

Complex logic (authentication, persistence, game mechanics) lives on your server or in your application.

License

MIT