PlayMeshClient
Import from @playmesh/client.
Constructor
import { PlayMeshClient } from '@playmesh/client';
const client = new PlayMeshClient(options: PlayMeshClientOptions);
| Method / Property | Description | Returns |
|---|
url | Server URL. e.g. "https://game.example.com" | string |
auth | Auth payload for the server's authenticate hook, or a function called on every connection attempt (useful for token refresh). | object | () => object |
socket | Options forwarded to the underlying Socket.IO client. | Partial<SocketOptions> |
signing | Sign all application events with ECDSA P-256. The server must enable signing too. | boolean |
Connection
| Method / Property | Description | Returns |
|---|
connect() | Connect and authenticate. Resolves with SessionInfo once the server establishes the session. | Promise<SessionInfo> |
disconnect() | Disconnect from the server. | void |
connected | Whether the client is currently connected. | boolean |
const session = await client.connect();
// session: { id: string, userId: string, instances: string[] }
console.log('Connected as', session.userId);
console.log('Instances:', session.instances);
client.disconnect();
Session
| Method / Property | Description | Returns |
|---|
session | Current session info. Updated automatically as instances change. | SessionInfo | undefined |
instances | Instance paths ("domainId/instanceId") this session belongs to. | string[] |
Events
| Method / Property | Description | Returns |
|---|
emit(event, payload?) | Send an event to the server. Throws if not connected. | void |
on(event, handler) | Listen for an event from the server. | this |
off(event, handler) | Remove a listener. | this |
join(instancePath) | Ask to join an instance. The server can veto; rejects with the denial message. | Promise<void> |
leave(instancePath) | Ask to leave an instance. Always honored. | Promise<void> |
chat(text) | Send a chat message to all instances you are in (server-moderated). | void |
client.on('chat', payload => {
const { sender, message } = payload as { sender: string; message: string };
appendMessage(`${sender}: ${message}`);
});
client.emit('move', { x: 100, y: 250 });
const handler = (payload: unknown) => { /* ... */ };
client.on('update', handler);
client.off('update', handler);
Connection lifecycle
| Method / Property | Description | Returns |
|---|
onDisconnect(handler) | Called when the connection drops. Receives the reason string. | this |
onReconnect(handler) | Called when the connection is automatically re-established. | this |
onError(handler) | Called when the server reports an error. | this |
onKick(handler) | Called when the server kicks this client, just before the disconnect. | this |
client.onDisconnect(reason => {
console.warn('Disconnected:', reason);
showReconnectUI();
});
client.onReconnect(() => {
hideReconnectUI();
});
client.onError(error => {
// { scope: 'connection' | 'event' | 'join' | 'chat',
// event?, instance?, message }
console.error('Server error:', error.message);
});
Authentication
// Static token
const client = new PlayMeshClient({
url: 'https://game.example.com',
auth: { token: localStorage.getItem('jwt') },
});
// Dynamic - refreshed on each reconnect
const client = new PlayMeshClient({
url: 'https://game.example.com',
auth: async () => ({ token: await authService.getToken() }),
});
Instance membership
The client automatically maintains client.instances as the server sends playmesh:joined and playmesh:left events. Clients can also request to join or leave instances:
await client.join('world/vip-lounge'); // rejects with the server's denial message
await client.leave('world/vip-lounge'); // always honored
ℹThe server stays authoritative: client joins are denied by default, unless the instance opts in with an onJoinRequest hook server-side.
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
});
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 broadcasts to every instance you are a member of. The server moderates each message in real time and may rewrite it, block it, or kick you.
client.chat('hello everyone');
client.onChat(message => console.log(`${message.userId}: ${message.text}`));
client.onKick(reason => console.log('Kicked:', reason));
Typed events
Pass event maps to type emit/on end to end (compile-time only).
type ClientEvents = { 'player:move': { x: number; y: number } };
type ServerEvents = { 'player:update': { x: number; y: number; by: string } };
const client = new PlayMeshClient<ClientEvents, ServerEvents>({ url });
client.emit('player:move', { x: 1, y: 2 }); // payload type-checked
Types
interface SessionInfo {
id: string;
userId: string;
instances: string[]; // "domainId/instanceId" paths
}
interface ServerError {
scope: 'connection' | 'event' | 'join' | 'chat';
event?: string;
instance?: string; // set when scope is 'join'
message: string;
}
interface ChatMessage {
instance: string;
sessionId: string;
userId: string;
text: string;
at: number; // server timestamp (epoch ms)
}
interface PresenceEvent {
instance: string;
type: 'join' | 'leave';
sessionId: string;
userId: string;
count: number; // cluster-wide, after this change
}
interface PresenceInfo {
count: number;
users: string[]; // unique user ids currently in the instance
}
interface StateChange {
instance: string;
scope?: 'user'; // marks your private per-user state
key?: string; // absent for a clear
value?: unknown; // absent on delete
cleared?: boolean;
}
Full example
◆game-client.ts
import { PlayMeshClient } from '@playmesh/client';
const client = new PlayMeshClient({
url: import.meta.env.VITE_SERVER_URL,
auth: async () => ({ token: await authService.getAccessToken() }),
});
client.on('world-state', payload => gameRenderer.updateWorld(payload));
client.on('player-joined', payload => gameRenderer.spawnPlayer(payload.userId));
client.on('player-left', payload => gameRenderer.removePlayer(payload.userId));
client.onDisconnect(() => gameRenderer.showDisconnectedOverlay());
client.onReconnect(() => gameRenderer.hideDisconnectedOverlay());
export async function startGame() {
const session = await client.connect();
gameRenderer.setLocalPlayerId(session.userId);
}
export const movePlayer = (x: number, y: number) => client.emit('move', { x, y });
export const sendChat = (message: string) => client.emit('chat', { message });