# PaindaProtocol Specification (LLM-Friendly) > This document is automatically generated for AI context acquisition. ## ๐Ÿ”„ Latest Protocol Improvements (Phase 5) - **Headless Injection**: `server.inject(virtualClient)` allows connecting headless bots directly to the middleware pipeline in-memory (0ms network latency). - **Delta Engine Scaling**: Cross-node scaling fixed via `PPAdapter.publish/subscribe`. Every server instance now relays remote room emissions to local sockets. - **Myers O(ND) Diffing**: Native array synchronization support for React state management (`patchImmutable`). - **Token Persistence**: `getToken()` is now called on every single reconnect to ensure fresh JWTs. - **Testing Suite**: New `@painda/testing` package with `createTestEnv` and `waitForMessage` for deterministic integration testing. --- ## Section: Overview redirect("/docs/quick-start"); } --- ## Section: benchmarks # Benchmarks & Performance PaindaProtocol is designed specifically for React-based multiplayer games and heavily relies on Operational Transformations and Myers Diff to drastically reduce network load. ## 1. The Payload Problem In a typical Socket.io game, sending 60 frames per second over WebSockets means blasting a massive `GameState` object to every client on every tick. This wastes CPU, hogs network bandwidth, and results in lag spikes for players on weak connections. Socket.io Timeline (100ms) ` [T+0ms]: emit("state", { players: [ ...100 items ] }) // 32 KB [T+16ms]: emit("state", { players: [ ...100 items ] }) // 32 KB [T+32ms]: emit("state", { players: [ ...100 items ] }) // 32 KB ` Total Bandwidth: ~2 MB/s per client. ## 2. The Delta Engine Solution PaindaProtocol features a highly optimized Delta Engine inside `@painda/gaming`. Using `server.emitDelta` automatically calculates deep object diffs before pushing to the network. PaindaProtocol Timeline (100ms) ` [T+0ms]: emitDelta("state", { players: [ ...100 items ] }) // 32 KB (baseline) [T+16ms]: emitDelta("state", { players: [ { x: 10, y: 15 } ] }) // 40 bytes! [T+32ms]: emitDelta("state", undefined) // 0 bytes (no changes!) ` Total Bandwidth: ~0.08 MB/s per client (96% reduction!) ## 3. React Integration: O(ND) Myers Diff Sending raw diffs works well for objects, but Array splicing usually breaks React states if not handled immutably. We ported the Myers O(ND) Diffing Algorithm to efficiently detect Array inserts and deletes. When you use `patchImmutable()`, it reads these Array splice operations and generates a pristine, deep-cloned React state that perfectly matches the server, causing zero unnecessary re-renders. ```typescript const [gameState, setGameState] = useState(); useEffect(() => { client.on("game:delta", (delta) => { // Automatically applies Myers array splices to React State setGameState(prev => patchImmutable(prev, delta)); }); }, []); ``` ); } --- ## Section: features/acks
# Acknowledgements Request-response pattern over WebSocket. Send a message and receive a confirmation callback with automatic timeout. ## Client โ†’ Server `// Client sends with callback client.send( { type: "save-profile", payload: { name: "Alex" } }, (err, response) => { if (err) { console.error("Save failed or timed out:", err.message); } else { console.log("Profile saved:", response); } } );` ## Server responds ```typescript `// Inside namespace connection handler nsSocket.on("save-profile", (msg) => { const ackId = (msg as any).__ackId; if (ackId) { // Process and send ack response saveToDb(msg.payload); nsSocket.sendAck(ackId, { success: true, id: 123 }); } });` ``` ## Timeout

Acks time out after 10 seconds by default. Configure via `ackTimeout` in client options: `const client = new PPClient({ url: "ws://localhost:3000", ackTimeout: 5000, // 5 seconds });`

Next:{" "} Connection State Recovery

); } --- ## Section: features/middleware
# Middleware Express-style middleware pipeline. Two types: connection middleware (runs on connect) and message middleware (runs on every message). ## Connection Middleware `// Global โ€” runs for all connections server.use((socket, next) => { console.log("New connection:", socket.id); next(); // Allow connection }); // Auth middleware server.use(async (socket, next) => { const token = getToken(socket); if (!token) return next(new Error("No token")); try { const user = await verifyToken(token); (socket as any).user = user; next(); } catch { next(new Error("Invalid token")); } }); // Namespace-specific middleware const admin = server.of("/admin"); admin.use((socket, next) => { if ((socket as any).user?.role === "admin") next(); else next(new Error("Admin access required")); });` ## Message Middleware ```typescript `// Global message middleware โ€” rate limiting, logging, etc. server.useMessage((socket, message, next) => { console.log(\`[\${socket.id}] \${message.type}\`); next(); }); // Namespace-specific message middleware admin.useMessage((socket, message, next) => { if (message.type.startsWith("admin:")) next(); else next(new Error("Invalid message type for admin")); });` ```

If any middleware calls `next(error)`, the connection is rejected or the message is dropped, and the client receives a `__pp_error` event. Next:{" "} Acknowledgements

); } --- ## Section: features/namespaces
# Namespaces Namespaces let you split your application logic over a single WebSocket connection. Each namespace has its own event handlers, middleware, and rooms. ## Server ` const server = new PPServer({ port: 3000 }); // Default namespace "/" server.on("connection", (client) => { console.log("Client joined default namespace"); }); // Custom namespace "/admin" const admin = server.of("/admin"); // Namespace-specific middleware admin.use((socket, next) => { if (isAdmin(socket)) next(); else next(new Error("Forbidden")); }); admin.on("connection", (nsSocket) => { nsSocket.on("stats", (msg) => { nsSocket.send({ type: "stats", payload: getStats() }); }); }); // Custom namespace "/game" const game = server.of("/game"); game.on("connection", (nsSocket) => { nsSocket.on("move", (msg) => { // Broadcast to all in this namespace game.broadcast(msg, nsSocket.id); }); });` ## Client ```typescript ` const client = new PPClient({ url: "ws://localhost:3000" }); // Send to a specific namespace client.send( { type: "stats", payload: {} }, { namespace: "/admin" } ); // Default namespace client.send({ type: "chat", payload: "Hello" });` ```

Next:{" "} Middleware Pipeline

); } --- ## Section: features/plugins
# Plugin System Extend PaindaProtocol with custom plugins. Plugins get full access to lifecycle hooks and can expose public APIs for other plugins. ## Creating a Plugin ` const myPlugin: PPPlugin<{ debug: boolean }> = { name: "my-plugin", version: "1.0.0", dependencies: [], // Other plugins required install(ctx, options) { // Register middleware ctx.use((socket, next) => { if (options?.debug) ctx.log("New connection:", socket.id); next(); }); // Expose API for other plugins ctx.expose({ getStats: () => ({ clients: ctx.getClientCount() }), }); // Return lifecycle hooks return { onConnect: (socket) => { /* ... */ }, onDisconnect: (socket) => { /* ... */ }, onMessage: (socket, msg) => { // Return false to block the message if (msg.type === "banned") return false; }, onSend: (socket, msg) => { // Transform outgoing messages return { ...msg, payload: { ...msg.payload, ts: Date.now() } }; }, onShutdown: () => { /* cleanup */ }, }; }, }; server.register(myPlugin, { debug: true });` ## Lifecycle Hooks
Hook When
onConnectAfter middleware, before handlers
onDisconnectClient disconnected
onMessageEvery incoming message (return false to block)
onSendBefore encoding outgoing message
onRoomJoin / onRoomLeaveRoom membership changes
onShutdownServer is shutting down
onErrorServer errors

Next:{" "} Typed Rooms

); } --- ## Section: features/presence
# Presence Track who's online with arbitrary metadata. Perfect for "user is typing", cursor sharing, and online status indicators. ## Server `const server = new PPServer({ port: 3000, presence: { syncInterval: 1000 }, // Broadcast every 1s }); server.on("connection", (client) => { // Track with arbitrary metadata server.presence.track(client, { name: "Alex", status: "online", cursor: { x: 0, y: 0 }, }); // Update metadata client.on("message", (msg) => { if (msg.type === "cursor") { server.presence.update(client.id, { cursor: msg.payload, }); } }); }); // Listen for changes server.presence.onChange((presences) => { console.log(\`\${presences.length} users online\`); }); // Auto-untracked on disconnect` ## Client ```typescript `// Presence list auto-synced client.on("presence", ({ presences }) => { for (const p of presences) { console.log(p.data.name, "is", p.data.status); renderCursor(p.data.cursor); } });` ```

Back to:{" "} @painda/core

); } --- ## Section: features/recovery
# Connection State Recovery When a client disconnects and reconnects, PaindaProtocol can automatically replay all missed messages and restore room memberships. No manual bookkeeping needed. ## Enable on Server `const server = new PPServer({ port: 3000, recovery: true, // Enable with defaults }); // Or configure: const server = new PPServer({ port: 3000, recovery: { maxBufferSize: 200, // Max messages to buffer per client retentionMs: 120_000, // Keep data for 2 minutes after disconnect }, });` ## Client Side ```typescript `const client = new PPClient({ url: "ws://localhost:3000", reconnect: true, }); client.on("recovery", ({ sid, recovered, missedCount }) => { if (recovered) { console.log(\`Session restored! \${missedCount} messages replayed.\`); } }); // After reconnect, the client automatically: // 1. Sends its session ID and last message offset // 2. Receives all missed messages in order // 3. Rejoins all previous rooms` ```

Recovery is transparent โ€” the client continues as if the disconnect never happened. If the retention window expires, a fresh session is created instead. Next:{" "} Scaling with Adapters

); } --- ## Section: features/rooms
# Typed Rooms Typed rooms combine state management with automatic delta broadcasting at 60 FPS. Define a TypeScript interface for your room state, and PP handles the rest. ## Server `interface GameState { phase: "waiting" | "playing" | "ended"; score: Record; timer: number; } const lobby = server.room("lobby-1", { phase: "waiting", score: {}, timer: 60, }, { maxClients: 10, tickRate: 16 }); // 60 FPS // Join clients (full state auto-synced) server.on("connection", (client) => { lobby.join(client); // Client gets full state immediately }); // Update state โ€” deltas auto-broadcast lobby.update(s => { s.phase = "playing"; s.score["player1"] = 10; }); // Room events lobby.on("join", (client) => console.log(client.id, "joined")); lobby.on("leave", (client) => console.log(client.id, "left")); // Lock room lobby.lock(); // No new joins lobby.unlock();` ## Client ```typescript `// Full state on join client.on("roomState", ({ room, state }) => { localState = state; // Initial sync }); // Deltas at 60 FPS client.on("roomDelta", ({ room, delta }) => { // Apply only the changed fields Object.assign(localState, delta); render(localState); });` ```

Next:{" "} Presence

); } --- ## Section: features/scaling
# 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` ```typescript ` 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. ` 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:
Bytes Field
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; subscribe(channel, callback): Promise; unsubscribe(channel): Promise; addToRoom(room, clientId): Promise; removeFromRoom(room, clientId): Promise; getClientsInRoom(room): Promise>; getClientRooms(clientId): Promise>; close(): Promise; }`

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

); } --- ## Section: features/virtual-clients # Virtual Clients A massive architectural advantage of PaindaProtocol is its native support for headless WebSocket connections via `PPVirtualClient`. ## What is a Virtual Client? A Virtual Client behaves exactly like a real user connecting over a WebSocket. It triggers your rate limiters, auth middleware, and presence mechanisms. However, it completely bypasses the physical network stack. The connection occurs synchronously in memory. ## Why Use Virtual Clients? - AI Game Bots: Connect NPCs to your game world without wasting open file descriptors or loopback network sockets. - End-to-End Testing: Spin up hundreds of test clients synchronously within Jest or Vitest without starting a real HTTP server. - SSR Data Hydration: Have a Next.js server component transparently connect, fetch the lobby state, and render it server-side. ## Basic Integration To connect a Virtual Client, initialize it and simply pass your active `PPServer` instance to the `connect()` method. const server = new PPServer({ port: 3000 }); // 1. Create a headless client const bot = new PPVirtualClient("AI_BOT_01"); // 2. Setup listeners (just like a real client) bot.on("game:state", (state) => { console.log("Bot sees the state:", state); }); // 3. Inject it straight into the server pipeline bot.connect(server); // 4. Send messages seamlessly bot.send({ type: "player:move", payload: { x: 10, y: 10 } }); ## Middleware Pipeline

Virtual Clients pass through identical middleware to real clients. This ensures your bots respect Authentication schemas, bans, and custom connection rules. We simulate the source IP of a virtual client as `127.0.0.1` to safely pass standard validations. ); } --- ## Section: guides/socket-io-migration # Migrating from Socket.io PaindaProtocol (PP) was heavily inspired by Socket.io and purposefully adopts an identical API surface for its core features. This makes migrating the majority of your business logic trivial. ## 1. Installation ` - npm uninstall socket.io socket.io-client + npm install @painda/core @painda/client ` ## 2. Server Setup Socket.io creates its own HTTP server automatically if you pass a port. PaindaProtocol does the same. If you previously wrapped an Express server, you can do this identically via `attachTo`. ### Socket.io const httpServer = createServer(app); const io = new Server(httpServer, { cors: { origin: "*" } }); ### PaindaProtocol ```typescript const httpServer = createServer(app); const io = PPServer.attachTo(httpServer, { allowedOrigins: ["*"] }); ``` ## 3. Client Setup & Auth

In Socket.io you pass `auth: { token }` which gets evaluated once. PaindaProtocol uses a dynamic `getToken` callback so that your token is freshly gathered on every subsequent reconnect. This forces the client to automatically fetch a new token if the JWT expired while disconnected. ### Socket.io const socket = io("ws://localhost:3000", { auth: { token: "my-jwt-token" } }); ### PaindaProtocol ```typescript const socket = new PPClient({ url: "ws://localhost:3000", // Called initially and on EVERY reconnect getToken: async () => localStorage.getItem("token") }); ``` ## 4. Namespaces & Broadcasts (Zero Changes)

The massive upside of migrating to PaindaProtocol is that almost all your socket interaction logic remains untouched. // Works identically in both libraries: io.on("connection", (socket) => { // Join rooms socket.join("lobby-1"); // Broadcast to room io.to("lobby-1").emit("chat", "Hello Room!"); // Broadcast to all except sender socket.broadcast.emit("player_join", socket.id); // Event listeners socket.on("action", (data) => { console.log(data); }); // Disconnect socket.on("disconnect", () => { console.log("Left!"); }); }); ## 5. Upgrade to Delta-State Sync

Once migrated, you can instantly benefit from the Delta Engine without altering your data format. Instead of broadcasting full states: ```typescript // Server io.to(room).emitDelta("update", oldState, newState); // React Client client.on("update", (delta) => { setGameState(prev => patchImmutable(prev, delta)); }); ``` ); } --- ## Section: modules/admin

# @painda/admin Lightweight monitoring dashboard for PaindaProtocol servers. Zero runtime dependencies โ€” built on plain `node:http`. Serves a self-contained HTML dashboard and a Prometheus-compatible metrics endpoint. ## Installation `npm install @painda/admin` ## Basic Setup ```typescript ` const server = new PPServer({ port: 3000 }); // Optional: register metrics plugin for /api/stats and /metrics server.register(ppMetricsPlugin); const admin = new PPAdminServer(server, { port: 9090, host: "localhost", // default auth: { username: "admin", password: "secret", }, }); admin.start(); // Dashboard: http://localhost:9090 // Prometheus: http://localhost:9090/metrics` ``` ## HTTP Endpoints
Route Response Notes
GET /HTML dashboardAuto-refreshes every 2s, no CDN deps
GET /api/statsJSONServer stats + metrics snapshot + plugin names
GET /api/clientsJSON arrayConnected clients: id, rooms, tags
GET /metricsPrometheus textRequires `ppMetricsPlugin` โ€” 404 otherwise
## Options
Option Type Default Description
portnumber9090HTTP port for the admin server
hoststring"localhost"Bind address
auth.usernamestringโ€”Basic Auth username (omit to disable auth)
auth.passwordstringโ€”Basic Auth password
## Dashboard Stats Payload (`/api/stats`) ```typescript `{ "clients": 42, // connected client count "rooms": 8, // active room count "presenceTracked": 40, // clients tracked by presence "uptime": 3612, // server uptime in seconds "plugins": ["pp-metrics", "pp-auth"], "metrics": { // null if ppMetricsPlugin not registered "messagesReceived": 14500, "messagesSent": 28900, "bytesReceived": 450000, "bytesSent": 890000, "connectionsTotal": 150, "disconnectionsTotal": 108, "roomJoinsTotal": 320, "roomLeavesTotal": 310, "errorsTotal": 2 } }` ``` ## API
Member Description
new PPAdminServer(server, opts)Create admin server โ€” does not start listening yet
admin.start()Start listening; logs dashboard and metrics URLs
admin.stop()`Promise` โ€” graceful shutdown
admin.httpServerUnderlying `http.Server` instance for advanced use

See also:{" "} Plugins {" "}ยท{" "} @painda/core

); } --- ## Section: modules/auth # @painda/auth (Enterprise) Secure your PaindaProtocol connections at the lowest level before the socket upgrade is fully accepted.
## Overview The `PPAuthMiddleware` sits directly on top of the WebSocket connection layer. By acting as an interceptor, it validates JSON Web Tokens (JWT) or custom session objects before any application-level `onMessage` listeners are fired. ### apps/server/index.ts ```typescript ` const server = new PPServer({ port: 7000 }); // Secure the server with zero boilerplate new PPAuthMiddleware(server, { validator: async (token) => { return await verifyJwt(token, process.env.JWT_SECRET); }, allowGuest: false });` ```
## Key Features - Zero-Trust Architecture: Drops unauthenticated sockets instantly without burning CPU cycles parsing complex application messages. - Context Injection: The validated user profile is automatically injected into the socket object: `client.userContext`. - Guest Mode: Pass `allowGuest: true` to allow read-only connections or limited permissions.
); } --- ## Section: modules/chat # @painda/chat Full-featured room management and direct messaging built on the blazing fast Painda core. ## Features - RoomManager: Easily join, leave, and broadcast to specific rooms just like Socket.io. - Direct Messaging: Send targeted whispers / private messages between specific client connection IDs. - Decoupled: Use it alongside the Gaming or Video modules seamlessly. ## Usage Example ```typescript ` const server = new PPServer({ port: 7001, registry }); const rooms = new RoomManager(server); server.on("connection", (client) => { // Join a room rooms.join(client, "global-lobby"); // Broadcast to the room (excluding sender) rooms.broadcastToRoom("global-lobby", { type: "chat-message", payload: "Hello world!" }, client); // Leave room rooms.leave(client, "global-lobby"); });` ``` ); } --- ## Section: modules/core # @painda/core The blazing fast binary backbone of the PaindaProtocol ecosystem. Now with full Socket.io feature parity. ## Why @painda/core? - Zero-Copy Architecture: Reads direct ArrayBuffers instead of expensive string parsing. - Namespaces: Multiplex `server.of("/admin")` over a single connection. - Middleware Pipeline: Express-style `server.use()` chains for auth & validation. - Acknowledgements: Request-response with `client.send(msg, callback)`. - Typed Contracts: Schema registry ensures binary type safety across the wire. - Connection Recovery: Missed messages replayed automatically after reconnect. - Horizontal Scaling: Adapter interface for Redis / Postgres multi-instance deployments. ## Installation `npm install @painda/core` ## Server with Namespaces & Middleware ```typescript ` const server = new PPServer({ port: 7001, registry, heartbeatInterval: 30000, // Ping every 30s maxMessagesPerSecond: 100, // Rate limiting recovery: true, // Enable state recovery }); // Global middleware โ€” runs for every connection server.use((socket, next) => { console.log("Client connected:", socket.id); next(); }); // Namespaces โ€” split logic over one connection const admin = server.of("/admin"); admin.use((socket, next) => { // Namespace-specific auth if (isAdmin(socket)) next(); else next(new Error("Unauthorized")); }); admin.on("connection", (nsSocket) => { nsSocket.on("dashboard:stats", (msg) => { // Handle admin-only events }); }); // Default namespace server.on("connection", (client) => { client.on("message", (msg) => { server.broadcast(msg, client); // Exclude sender }); });` ``` ## Client with Acks & Recovery ```typescript ` const client = new PPClient({ url: "ws://localhost:7001", registry, reconnect: true, ackTimeout: 5000, }); // Send with acknowledgement callback client.send( { type: "save", payload: { name: "test" } }, (err, response) => { if (err) console.error("Server didn't respond:", err); else console.log("Confirmed:", response); } ); // Volatile send โ€” dropped if disconnected (no queue) client.send( { type: "cursor", payload: { x: 100, y: 200 } }, { volatile: true } ); // Catch-all for debugging client.onAny((event, ...args) => { console.log("Event:", event, args); }); // Recovery event after reconnect client.on("recovery", ({ missedCount }) => { console.log(\`Recovered \${missedCount} missed messages\`); });` ``` ## Compression

Frame-level deflate compression is active by default for payloads over 1 KB. Uses `node:zlib` on the server side and the `DecompressionStream` Web API in the browser client. Saves 60โ€“80% bandwidth on JSON payloads (game state, chat history, etc.). `const server = new PPServer({ port: 3000, compression: { algorithm: "deflate", // "deflate" | "none" (default: "deflate") threshold: 1024, // min bytes before compressing (default: 1024) }, }); // Compression is per-frame: only applied if compressed.byteLength < original.byteLength // perMessageDeflate is disabled on the WS layer โ€” PP handles compression itself`

Compression is applied at the PP frame level, not the WebSocket level (`perMessageDeflate: false`). This means compression and PP's binary framing work independently of the WebSocket transport. ## Room API (Socket.io-compatible) ```typescript `// On socket (Socket.io-style): socket.join("game-1") socket.leave("game-1") socket.to("game-1").emit("update", delta) // to room, excluding self socket.broadcast.emit("event", data) // to all, excluding self // On server: server.to("game-1").emit("update", delta) server.in("game-1").fetchSockets() // Promise server.in("game-1").disconnectSockets() server.in(["r1", "r2"]).emit("event", data) // multiple rooms server.except("admins").emit("msg", data) // all except a room server.in("r1").except("r2").emit("msg", d) // chaining` ``` ## Full API Surface
API Description
server.of(name)Create / get namespace
server.use(fn)Global connection middleware
server.useMessage(fn)Global message middleware
server.broadcast(msg)Encode-once broadcast to all
server.broadcastVolatile(msg)Broadcast, drop on failure
server.onAny(fn)Catch-all event listener
client.send(msg, cb)Send with ack callback
client.send(msg, {volatile})Volatile send (drop if busy)
client.onAny(fn)Catch-all listener
client.recoverySessionIdCurrent recovery session
server.getClients()All connected `PPClientSocket[]`
server.getPluginNames()Names of all registered plugins
server.getStats()Live server stats (clients, rooms, uptime, presenceTracked)
server.getPlugin(name)Get typed plugin API by name
ppMetricsPluginBuilt-in metrics plugin โ€” register with `server.register(ppMetricsPlugin)`
); } --- ## Section: modules/gaming # @painda/gaming The Delta Engine. Real-time state synchronization with minimal bandwidth โ€” object diffs, array delta ops, and Myers-optimal patching. ## Why the Delta Engine? - Bandwidth Saver: At 60 FPS, sending full JSON every frame kills the network. Only changed fields are sent. - Array Delta Ops: Arrays are diffed with Myers O(ND) โ€” only splice/set operations sent, not the full array. - Deletion Sentinels: Deleted keys are marked with `PP_DELETED`, not `null` โ€” survives JSON round-trips safely. ## Object Diff โ€” Server & Client Server: create and broadcast patches ` const gameState = new StateManager({ players: { hero1: { x: 10, y: 20, hp: 100 } } }); // Game loop tick gameState.update({ players: { hero1: { x: 15 } } }); // only X changed const delta = gameState.getDelta(); if (delta) { // { players: { hero1: { x: 15 } } } โ€” only the diff server.broadcast({ type: "game-update", payload: delta }); } `

Client: apply patches in-place ` let localState = { players: { hero1: { x: 10, y: 20, hp: 100 } } }; client.on("message", (msg) => { if (msg.type === "game-update") { patch(localState, msg.payload); // in-place mutation // localState.players.hero1.x is now 15 } });` ## Array Delta Ops

Arrays are diffed with Myers O(ND) algorithm. Instead of sending the whole array, only splice and set operations are transmitted. This is unique to PaindaProtocol โ€” no other WebSocket framework has built-in array-level delta sync. ```typescript ` const prev = { items: ["a", "b", "c", "d"] }; const next = { items: ["a", "x", "c", "d"] }; // only index 1 changed const delta = diff(prev, next); // delta.items === { __pp_array_ops: [{ op: "set", index: 1, value: "x" }] } // NOT the full array โ€” just the change patch(prev, delta); // prev.items === ["a", "x", "c", "d"]` ```
Export Description
PPArrayOpsMarkerType: `{ __pp_array_ops: ArrayOp[] }`
ArrayOp`{ op: "set", index, value }` or `{ op: "splice", index, deleteCount, items }`
isArrayOps(v)Type guard for `PPArrayOpsMarker`
Breaking change note: Server and client must use the same version of `@painda/gaming`. Array delta ops use a `__pp_array_ops` sentinel that older versions do not understand โ€” they will replace the array wholesale (backwards-compatible fallback). ## Deletion Sentinel ```typescript ` const prev = { score: 10, bonus: 5 }; const next = { score: 12 }; // bonus deleted const delta = diff(prev, next); // delta === { score: 12, bonus: PP_DELETED } // PP_DELETED = { __pp_deleted: true } โ€” NOT null patch(prev, delta); // prev === { score: 12 } // bonus removed` ``` Critical: Core's built-in diff uses `null` for deletions. If you use `patch` from `@painda/gaming` on the client, the server must also use `diff` from `@painda/gaming` (via `diffAlgorithm`). Mixing them silently breaks deletions. ); } --- ## Section: modules/persistence # @painda/persistence (Enterprise) Bridge the gap between ultra-fast in-memory processing and cold database storage.

## Overview Standard WebSocket servers force you to bloat your message handlers with manual database inserts. The `@painda/persistence` middleware automates this. You define the message types you want persisted, and it handles the batching and writing in the background. ### apps/server/index.ts ```typescript ` const server = new PPServer({ port: 7000 }); // Automatically sync "chat-message" to Postgres asynchronously new PPPersistenceMiddleware(server, { adapter: new PostgresAdapter(process.env.DATABASE_URL), syncTypes: ["chat-message", "game-state-snapshot"] });` ```
## Supported Adapters - PostgreSQL / Prisma: Relational bridging. - Redis: Key-value persistence for ultra-hot state that needs to survive server reboots. - MongoDB: Document storage for nested JSON payloads.
); } --- ## Section: modules/redis
# @painda/redis Binary-native Redis adapter for horizontal scaling. Unlike Socket.io's Redis adapter which uses ` JSON.stringify` for every inter-node message, PP uses a compact binary wire format with optional schema registry integration. ## Installation `npm install @painda/redis ioredis` ## Basic Setup ```typescript ` const server = new PPServer({ port: 3000, adapter: new RedisAdapter({ host: "localhost", port: 6379, // password: "...", // db: 0, // keyPrefix: "pp:", // default onError: (err) => console.error("[Redis]", err), }), });` ``` ## With Schema Registry (Maximum Compression)

Pass the same schema registry to both the server and the adapter. Registered event types are encoded as 2-byte IDs in Redis pub/sub instead of full strings. ` const registry = new PPSchemaRegistry(); registry.register("player:move", structSerializer(1, [ { name: "x", type: "float32" }, { name: "y", type: "float32" }, ])); const server = new PPServer({ port: 3000, registry, adapter: new RedisAdapter({ host: "redis-host", registry }), }); // "player:move" โ†’ 2 bytes in Redis // vs Socket.io: JSON.stringify({ event: "player:move", ... })` ## Options
Option Type Default Description
hoststring"localhost"Redis host
portnumber6379Redis port
passwordstringโ€”Redis AUTH password
dbnumber0Database index
keyPrefixstring"pp:"Prefix for all Redis keys and channels
registryPPSchemaRegistryโ€”Schema registry for binary type IDs. Must match the server's registry.
onError(err) => voidโ€”Redis connection error handler
## Binary Wire Format

Each Redis pub/sub message is a compact binary buffer (big-endian):
Field Size Notes
excludeLenuint8Byte length of excludeClientId (0 = no exclude)
excludeIdN bytes utf8The client ID to exclude from delivery
typeIduint160 = string type, >0 = schema registry ID
typeLen + typeuint16 + utf8Only present when typeId = 0
payloadrestJSON bytes (no registry) or schema-binary (with registry)
## Architecture Notes The adapter internally uses two ioredis clients: one for commands and publishing (`pubClient`), and one dedicated to subscribe mode (`subClient`). This is required because ioredis in subscribe mode cannot issue any other commands. Room membership is stored in Redis Sets: `pp:room:{room}` and reverse-lookup `pp:client:{id}:rooms`. Pub/sub channels use `pp:ch:{channel}` to avoid key collisions. See also:{" "} Horizontal Scaling {" "}ยท{" "} @painda/core

); } --- ## Section: modules/testing # @painda/testing Testing real-time network interactions introduces flakiness due to unpredictable latency and non-deterministic event loops. `@painda/testing` offers headless server+client bootstrapping and deterministic message awaiters specifically designed for integration logic tests. ## Installation ` npm install --save-dev @painda/testing ` ## Usage in Integration Tests We strongly recommend using `waitForMessage` and `createTestEnv` in Jest / Vitest environments over asserting state inside synchronous callbacks. ```typescript describe("Room Broadcasts", () => { it("should push delta states down to connected players", async () => { const { server, client, cleanup } = await createTestEnv(); // Arrange: Game Mock server.on("connection", (socket) => { socket.join("lobby"); server.to("lobby").emit("system", "welcome!"); }); // Assert: Deterministic event listener wait const msg = await waitForMessage(client, "system"); expect(msg.payload).toEqual("welcome!"); cleanup(); }); }); ``` ## Core API - `createTestEnv(options)`: Starts a real server on a random port and attaches a single client automatically. Resolves when the connection is open. - `createTestClients(port, count)`: Attaches multiple disconnected mock-peers instantly. - `waitForMessage(client, type)`: Promise-based event listener yielding the payload explicitly. - `collectMessages(client, count)`: Awaits an exact count of events regardless of the type. Useful for heartbeat tests or network flooding simulations. - `waitFor(fn)`: Generic polling assertion for hidden internal back-end states. ); } --- ## Section: modules/video # @painda/video A fast WebRTC signaling server built directly into PaindaProtocol. ## How it works - Out-of-the-Box Signaling: WebRTC requires a central server to exchange SDP Offers, Answers, and ICE candidates before establishing a P2P connection. - Room Based: Clients join a "Call Room" and the Signaling server automatically targets the right peers. - Zero Setup routing: The \`SignalingServer\` class handles all message delegation for you. ## Server Usage ```typescript ` const server = new PPServer({ port: 7001, registry }); const rtcManager = new SignalingServer(server); server.on("connection", (client) => { client.on("message", (msg) => { if (msg.type === "rtc-signal") { // Handles join, leave, offer, answer, and candidate routing rtcManager.handleSignal(client, msg); } }); }); ` ``` ); } --- ## Section: protocol/encoding # Encoding & Wire Format PaindaProtocol is built to minimize serialization overhead and bandwidth usage. It uses a custom Binary layout to achieve 70-90% smaller payloads compared to standard JSON WebSockets. ## The Binary Frame (v2) Instead of passing raw JSON strings natively over WebSockets, Painda encodes every message into a strict Binary Frame starting with a 16-byte header:
Bytes Field Description
0-3 Magic Bytes `0x50504E44` ("PPND") - Identifies a valid Painda frame.
4-5 Version (uint16) Wire protocol version (currently `2`).
6-7 Flags (uint16) Bits 0-1 mode, Bit 2 compression, Bit 3 custom schema encoding.
8-11 Length (uint32) Length of the following payload array.
12-13 Type ID (uint16) `0` = JSON fallback, `>0` = Custom Schema ID.
14-15 Reserved (uint16) Reserved for future expansion.
16+ Payload The encoded message bytes.
## Fallback: JSON mode By default, if you don't define a custom schema, PaindaProtocol uses JSON encoding wrapped inside the binary frame. This means the header routing is hyper-fast, but the payload itself is standard `TextEncoder().encode(JSON.stringify(msg))`. This provides perfect Developer Experience (DX) for modern apps without writing schemas. ## Custom Schemas (Maximum Speed) For high-frequency game packets (like player movement), JSON is too bloated. PaindaProtocol allows you to register custom binary schemas using the `PPSchemaRegistry`. ` const { PPSchemaRegistry, structSerializer } = require("@painda/core/schema"); // 1. Create a registry const registry = new PPSchemaRegistry(); // 2. Register your high-freq payload registry.register("player_move", 1, structSerializer([ { name: "x", type: "float32" }, { name: "y", type: "float32" } ])); // Now { type: "player_move", payload: { x: 10.5, y: -4.2 } } // is encoded into exactly 8 bytes of payload instead of ~50 bytes of JSON! ` ### Compression Support

If your payload exceeds the compression threshold, the server automatically compresses it using Zlib/Deflate (for Node.js) and sets the `FLAG_COMPRESSED` bit. Browser clients decompress this seamlessly using the Native Web `DecompressionStream` API. ); } --- ## Section: protocol/error-handling # Error Handling Errors happen. PaindaProtocol captures server-side logic and validation errors and safely transmits them back to the client. ## 1. Acknowledgement Errors When using Request/Response patterns (Acknowledgements), the first argument in the callback is reserved for the Error, following the standard Node.js convention (`error-first callbacks`). ` // Client client.send({ type: "authenticate", payload: "fake-jwt" }, (err, data) => { if (err) { // The server called ack(new Error("...")) console.error("Auth Error:", err.message); return; } console.log("Welcome!", data); }); // Server server.on("authenticate", (client, token, ack) => { if (!isValid(token)) { // Sending a standard Error object will be serialized automatically ack(new Error("Invalid authorization token provided")); } else { ack(null, { status: "success" }); } }); ` ## 2. Global Client Errors

Some errors don't belong to a specific callback. For example: malformed message frames, connection timeouts, or protocol version mismatches. ` client.on("error", (err) => { // Global catch for connection and parsing errors console.error("PaindaProtocol exception:", err); }); ` ## 3. Disconnect Reasons

When a connection drops, the `disconnect` event emits a reason string. This helps you distinguish between network issues and intentional kicks. ` client.on("disconnect", (reason) => { if (reason === "transport close") { console.log("Server crashed or network dropped."); } else if (reason === "Ping timeout") { console.log("Client failed to respond to server pings."); } else if (reason === "forced server disconnect") { console.log("You were kicked by the server-side logic."); } }); ` ### Uncaught Server Exceptions

If your server throws an unhandled synchronous exception inside an event handler, PaindaProtocol will catch it and prevent the Node.js process from crashing. It will automatically log the error under the hood. However, for `async/await` handlers, make sure to wrap your code in try/catch blocks! ); } --- ## Section: protocol/lifecycle # Connection Lifecycle PaindaProtocol is designed to keep connections alive and seamlessly reconnect when clients drop. This page explains the standard lifecycle from connecting to disconnecting. ## 1. Handshake (No HTTP Polling) Unlike other frameworks, PaindaProtocol does not start with a 3 RTT (Round Trip Time) HTTP polling phase. It attempts to open a pure `WebSocket` connection immediately. ` // This immediately attempts a ws:// or wss:// connection const client = new PPClient("ws://localhost:7000"); client.on("connect", () => { console.log("Connected directly! Id:", client.id); }); ` ## 2. Automatic Reconnects

In the real world, connections drop (e.g. driving through a tunnel on mobile, or server restarts). PaindaProtocol manages these drops automatically.

Reconnect Configuration

`reconnect: boolean` - Enable/disable auto-reconnect (default: true). `reconnectAttempts: number` - Max attempts before giving up (default: Infinity). `reconnectDelay: number` - Base delay in milliseconds (default: 1000). ` const client = new PPClient("wss://api.example.com", { reconnect: true, reconnectDelay: 2000, }); client.on("disconnect", (reason) => { console.log("Disconnected:", reason); // e.g. "transport close" }); client.on("reconnect", (attempt) => { console.log(\`Successfully reconnected on attempt \${attempt}\`); }); ` ## 3. Reconnect Strategy: Jitter + Backoff

When a server restarts, and 1000 clients attempt to reconnect immediately at exactly 1000ms, your server will be DDOSed by your own users (Thundering Herd Problem). PaindaProtocol adds random Jitter (typically ยฑ20%) to the `reconnectDelay`. It also supports Backoff. If attempt 1 fails at ~1000ms, attempt 2 might happen at ~2000ms, then ~4000ms, until maxing out at a sensible upper bound. ## 4. The Offline Buffer What happens if you emit a message while disconnected? ` // Client is currently OFFLINE client.emit("scoreUpdate", { points: 10 }); `

PaindaProtocol pushes this message into an internal Offline Buffer. As soon as the "reconnect" event fires, it automatically flashes the buffer to the server in order. ### State Recovery If you want to gracefully recover disconnected sessions (resume missed messages while disconnected), pair the basic lifecycle with the `@painda/recovery` or `@painda/persistence` Middleware on your Server! ); } --- ## Section: protocol/message-types # Message Types PaindaProtocol simplifies realtime communication by standardizing how messages are sent and acknowledged. Under the hood, all messages follow a unified `PPMessage` interface. ## The `PPMessage` Structure Every message sent over PaindaProtocol is parsed into a simple object: ` interface PPMessage { type: string; // The event name (e.g., "chat", "move", "join") payload: T; // The actual data } ` ## 1. Fire-and-Forget Events

The most common way to send data is a simple, one-way event. The server or client emits an event, and the other side listens for it. No confirmation is sent back. ` // Client sends a fire-and-forget event client.emit("playerMove", { x: 10, y: 20 }); // Server receives it server.on("playerMove", (client, payload) => { // Update state }); ` ## 2. Request / Response (Acknowledgements)

If you need to know that the other side received and processed your message, you can use Acknowledgements (Acks). You pass a callback function as the last argument, and the receiver can call it to send a response back. ` // Client sends a request expecting a response client.send({ type: "login", payload: { token: "123" } }, (err, response) => { if (err) console.error("Login failed or timed out"); else console.log("Login successful!", response); }); // Server handles the request and responds server.on("login", (client, payload, ack) => { const success = verify(payload.token); if (success) { ack(null, { userId: 1 }); // null = no error, followed by response payload } else { ack(new Error("Invalid token")); } }); ` ### Internal Implementation

Acks are implemented by injecting an internal `__ackId` property into the message. When the receiver calls the `ack()` function, PaindaProtocol automatically creates a new message with the type `__pp_ack` and routes it back to the original sender's pending callback list. ); } --- ## Section: protocol/versioning # Versioning PaindaProtocol is designed to evolve. To prevent horrible desync bugs when clients and servers run different versions, the protocol enforces strict version checks at the handshake and frame levels. ## 1. Wire Protocol Versioning Every binary frame sent over the network includes a 2-byte `Version Header`. If an old client connects to a new server (or vice versa) and the major wire protocol versions don't match, the connection is instantly rejected with a clear error.

Current Version

PaindaProtocol is currently on Wire Protocol v2. ` // Client attempts to connect client.on("disconnect", (reason) => { if (reason.includes("Invalid Protocol Version")) { console.error("Please refresh your browser. We pushed an update!"); } }); ` ## 2. Application Schema Versioning

Even if the wire protocol matches, your own game's packet schema might change (e.g., adding a "Z" axis to player movement). PaindaProtocol's `PPSchemaRegistry` supports built-in versioning for your custom payloads. When registering a schema, the second augment is the `version`. ` // Server-side (Game v2) registry.register("player_move", 2, structSerializer([ { name: "x", type: "float32" }, { name: "y", type: "float32" }, { name: "z", type: "float32" }, // NEW FIELD ])); // When an old v1 client sends a "player_move" frame, // the server detects the ID/Version mismatch and discards or downgrades it safely, // rather than reading out-of-bounds memory. ` ## 3. Dealing with Breaking Changes

Best practices for deploying updates to a live PaindaProtocol app: - Soft Updates: Add new optional fields to your JSON payloads. Older clients ignore them. - Hard Schema Updates: Bump the version number in `registry.register("name", VERSION, ...)`. Old payloads will be ignored. - Force Refresh: You can use a generic "version_check" event on connection to tell old clients to call `location.reload(true)`. ### Future Proof By baking versioning into the core 16-byte header, PaindaProtocol guarantees that your application state will never be corrupted by legacy packets lingering in the network after a hot-reload or blue/green deployment. ); } --- ## Section: quick-start

# Quick Start Get PaindaProtocol running and build a "Hello World" chat in under 2 minutes. ## Installation `npm install @painda/core` ## Hello World Chat

Create a minimal server and client that exchange a single binary-framed message. ### Server ` const server = new PPServer({ port: 3000 }); server.on('connection', (client) => { client.on('message', (msg) => { console.log('Received:', msg); // DX shorthand โ€” same as send({ type, payload }) client.emit('hello', 'World'); }); });` ### Client ```typescript ` const client = new PPClient({ url: 'ws://localhost:3000', }); // once() โ€” auto-removes after first call client.once('open', () => { // emit() shorthand client.emit('greet', 'Hello'); }); client.on('message', (msg) => { console.log('Server says:', msg.payload); });` ``` ## Typed Contracts

Register schemas for binary-native serialization with full TypeScript inference. No more JSON overhead. ` const registry = new PPSchemaRegistry(); registry.register('player:move', structSerializer(1, [ { name: 'x', type: 'float32' }, { name: 'y', type: 'float32' }, { name: 'z', type: 'float32' }, ]) ); const server = new PPServer({ port: 3000, mode: 'gaming', registry, }); server.on('connection', (client) => { client.on('message', (msg) => { if (msg.type === 'player:move') { const { x, y, z } = msg.payload; console.log(x, y, z); } }); // 12 bytes instead of ~50+ JSON bytes client.emit('player:move', { x: 1.5, y: 0, z: -3.2 } ); });`

The schema registry maps `player:move` to a compact 12-byte struct (3 x float32) instead of a ~50-byte JSON string. Unregistered types fall back to JSON automatically. Next, explore{" "} PP.Chat for rooms and history, or{" "} The PP Header for the binary wire format.

); } --- ## Section: sdks/cpp # โšก C++ SDK (Coming Soon) The performance pinnacle for PaindaProtocol. ### ๐Ÿ”ฅ Status: Strategic Planning We are defining the header-only architecture for the C++ Core SDK. This will be targetted for systems where every microsecond counts. ## Target Environments - Embedded Systems: Real-time telemetry and control. - High-Perf Game Engines: Custom C++ engines (Unreal, 4A, etc.). - High-Frequency Trading: Low-latency data distribution. ## Design Goals - Header-Only: Easy integration into any project with zero build system friction. - Zero-Allocation: Predictable performance without heap churn. - Compile-Time Safety: Binary templates for strict protocol compliance. ## Roadmap
    - Binary Header Spec V2 (Complete) - Reference implementation for framing and checksums - Delta Engine bit-manipulation library - Integration with `boost::asio` and `uWebSockets`
); } --- ## Section: sdks/csharp # ๐ŸŽฎ C# SDK (Coming Soon) Native, zero-copy messaging for Unity, Godot, and .NET. ### ๐Ÿ—“๏ธ Status: In Development The C# SDK is being architected specifically for gaming environments. We are targeting a Q2 2026 release with full Unity package support. ## Performance First PaindaProtocol is built for games. Our C# client focuses on: - Allocation-Free Logic: Utilizing `Span` and `Memory` to avoid GC pressure. - Unity Compatibility: Native support for `MonoBehaviour` and `Task`-based async patterns. - Delta Buffering: In-place state patching for 60 FPS state-sync. ## Technical Roadmap
    - Header V2 Binary Framing (Complete) - WebSocket Client Integration (In-Progress) - Delta Engine Patching Logic - Unity Package (.unitypackage / UPM) wrapper
## Unity Integration Preview ```typescript `// Example of what we are building public class GameClient : MonoBehaviour { private PPClient client; async void Start() { client = new PPClient("ws://localhost:3000"); await client.ConnectAsync(); client.On("roomState", (state) => { UpdateLevel(state); }); } }` ``` ); } --- ## Section: sdks/python # ๐Ÿ Python SDK (Beta) Bringing PaindaProtocol's zero-copy binary performance to the Python ecosystem. Ideal for AI integration, data science streaming, and high-performance backend services. ### ๐Ÿšง Current Status: Beta The core binary framing and async transport are implemented. We are currently finalizing the Delta Engine (state synchronization) parity. ## Key Features - Native Binary Framing: Efficient Header V2 encoding using `struct`. - Async First: Built on top of `asyncio` and `websockets`. - Zero-Copy Parity: Minimize memory allocations during frame processing. - Delta Engine Support: Compatible with JS-based `StateManager` for real-time sync. ## Quick Start (Preview)
        `{`
}