DocumentationCore Protocol

Horizontal Scaling

By default PP uses an in-memory adapter. For multi-instance deployments, swap in @painda/redis — a binary-native Redis adapter that is significantly faster than Socket.io's Redis adapter (which uses JSON for inter-node pub/sub).

Quick Setup

npm install @painda/redis
import { PPServer } from "@painda/core";
import { RedisAdapter } from "@painda/redis";

const server = new PPServer({
  port: 3000,
  adapter: new RedisAdapter({
    host: "localhost",
    port: 6379,
  }),
});

Maximum Compression with Schema Registry

Combine RedisAdapter with a schema registry to encode event types as 2-byte IDs instead of full strings — dramatically reducing Redis bandwidth at scale.

import { PPServer, PPSchemaRegistry, structSerializer } from "@painda/core";
import { RedisAdapter } from "@painda/redis";

const registry = new PPSchemaRegistry();
registry.register("player:move", structSerializer(1, [
  { name: "x", type: "float32" },
  { name: "y", type: "float32" },
]));

// Both server and adapter share the same registry
const server = new PPServer({
  port: 3000,
  registry,
  adapter: new RedisAdapter({ host: "redis", registry }),
});
// "player:move" encoded as 2-byte ID in Redis pub/sub
// vs. Socket.io: JSON.stringify({ type: "player:move", ... })

Binary Wire Format

Socket.io's Redis adapter uses JSON.stringify for every inter-node message. PP uses a custom compact binary format:

BytesField
uint8excludeLen — length of excludeClientId (0 = no exclude)
N bytesexcludeId — utf8 client ID to exclude
uint16typeId — 0 = string type, >0 = schema registry ID
uint16 + utf8type string (only when typeId = 0)
restpayload — JSON bytes (no registry) or schema-binary (with registry)

Adapter Interface

interface PPAdapter {
  publish(channel, message, excludeClientId?): Promise<void>;
  subscribe(channel, callback): Promise<void>;
  unsubscribe(channel): Promise<void>;
  addToRoom(room, clientId): Promise<void>;
  removeFromRoom(room, clientId): Promise<void>;
  getClientsInRoom(room): Promise<Set<string>>;
  getClientRooms(clientId): Promise<Set<string>>;
  close(): Promise<void>;
}

Implement this interface for any pub/sub backend (NATS, Postgres, etc.). When using an adapter, server.to(room).emit() and server.broadcast() automatically fan out to all instances in the cluster.

See full reference: @painda/redis · @painda/core