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
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
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
Next:{" "} Middleware Pipeline
| Hook | When |
|---|---|
| onConnect | After middleware, before handlers |
| onDisconnect | Client disconnected |
| onMessage | Every incoming message (return false to block) |
| onSend | Before encoding outgoing message |
| onRoomJoin / onRoomLeave | Room membership changes |
| onShutdown | Server is shutting down |
| onError | Server errors |
Next:{" "} Typed Rooms
Back to:{" "} @painda/core
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
Next:{" "} Presence
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 |
|---|---|
| uint8 | excludeLen โ length of excludeClientId (0 = no exclude) |
| N bytes | excludeId โ utf8 client ID to exclude |
| uint16 | typeId โ 0 = string type, >0 = schema registry ID |
| uint16 + utf8 | type string (only when typeId = 0) |
| rest | payload โ JSON bytes (no registry) or schema-binary (with registry) |
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
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
See also:{" "}
Plugins
{" "}ยท{" "}
@painda/core
## Options
Route
Response
Notes
GET / HTML dashboard Auto-refreshes every 2s, no CDN deps GET /api/stats JSON Server stats + metrics snapshot + plugin names GET /api/clients JSON array Connected clients: id, rooms, tags GET /metrics Prometheus text Requires `ppMetricsPlugin` โ 404 otherwise
## 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
Option
Type
Default
Description
port number 9090 HTTP port for the admin server host string "localhost" Bind address auth.username string โ Basic Auth username (omit to disable auth) auth.password string โ Basic Auth password
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 admin.httpServer Underlying `http.Server` instance for advanced use
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 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"]`
```
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
Each Redis pub/sub message is a compact binary buffer (big-endian):
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.
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
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.
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
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: 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 });
}
`
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.recoverySessionId Current 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 Get typed plugin API by name ppMetricsPlugin Built-in metrics plugin โ register with `server.register(ppMetricsPlugin)`
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.
Export
Description
PPArrayOpsMarker Type: `{ __pp_array_ops: ArrayOp[] }` ArrayOp `{ op: "set", index, value }` or `{ op: "splice", index, deleteCount, items }` isArrayOps(v) Type guard for `PPArrayOpsMarker`
## Binary Wire Format
Option
Type
Default
Description
host string "localhost" Redis host port number 6379 Redis port password string โ Redis AUTH password db number 0 Database index keyPrefix string "pp:" Prefix for all Redis keys and channels registry PPSchemaRegistry โ Schema registry for binary type IDs. Must match the server's registry. onError (err) => void โ Redis connection error handler
## 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
Field
Size
Notes
excludeLen uint8 Byte length of excludeClientId (0 = no exclude) excludeId N bytes utf8 The client ID to exclude from delivery typeId uint16 0 = string type, >0 = schema registry ID typeLen + type uint16 + utf8 Only present when typeId = 0 payload rest JSON bytes (no registry) or schema-binary (with registry)
## 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
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.
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
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
- 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
- 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)
`{`
}