A lightweight, cross-language messaging and RPC protocol designed used by vuer and zaku.
vuer-message-protocol/
├── README.md
├── vrpc-ts/          # TypeScript implementation
├── vrpc-py/          # Python implementation, uses uv
├── vrpc-rs/          # Rust implementation, uses cargo
├── vrpc-swift/       # Swift implementation, uses SPM
├── vrpc-cpp/         # C++ implementation (planned)
└── protocol/         # Shared protocol definitions
VMP uses MessagePack for efficient binary serialization and transport with Content-Type: application/msgpack.
MessagePack provides compact binary encoding while maintaining cross-language compatibility.
ZData (Zaku Data) is a wrapper format for encoding custom data types including NumPy arrays, PyTorch tensors, and PIL
images. The schema uses ztype as the discriminator field to avoid naming collisions with the existing dtype property
from NumPy and PyTorch.
// ZData wrapper format for special data types in Zaku
type ZData =
  | { ztype: "image"; b: Uint8Array }
  | { ztype: "numpy.ndarray"; b: Uint8Array; dtype: string; shape: number[] }
  | { ztype: "torch.Tensor"; b: Uint8Array; dtype: string; shape: number[] }Design Rationale: The ztype field indicates what kind of object was encoded, while dtype retains its original
meaning from NumPy/PyTorch to specify the element data type. NumPy dtype objects aren't serializable, so they are
converted to string representations (e.g., "float32", "int64", "uint8", "complex128") for transport. NumPy's
frombuffer() accepts these string dtype specifications and reconstructs the proper type.
Examples:
- NumPy array: {ztype: "numpy.ndarray", b: <binary>, dtype: "float32", shape: [224, 224, 3]}
- PyTorch tensor: {ztype: "torch.Tensor", b: <binary>, dtype: "int64", shape: [1, 512]}
The vuer component schema is a (mostly) nested dictionary of key-value pairs. The keys are strings and the values are arbitrary data types. The component looks like this:
| Component Schema | Allowed Value Types | 
|---|---|
| interface VuerComponent {
  tag: string;
  children: VuerComponent[];
  
  [key: string]: unknown
} | 
 | 
interface Message {
  ts: number;                       // timestamp
  etype: string;                    // event type or queue name
  rtype?: string;                   // response type (RPC)
  args?: any[];                     // positional arguments for RPC
  kwargs?: { [key: string]: any };  // keyword arguments
  data?: any;                       // server payload
  value?: any;                      // client payload
}
interface ClientEvent {
  ts: number;       // timestamp
  etype: string;    // event type
  rtype?: string;   // response type (RPC)
  value: any;       // client payload
}
interface ServerEvent {
  ts: number;       // timestamp
  etype: string;    // event type
  data: any;        // server payload
}The vuer message contains a timestamp (ts), event type (etype), server payload (data), and client payload (value).
| Message (Base) | ClientEvent | ServerEvent | 
|---|---|---|
| Generic message envelope with all possible fields | Client-to-server events with  | Server-to-client events with  | 
| RPC_Request | RPC_Response | 
|---|---|
| RPC request with  | RPC response where  | 
Vuer uses a client-server architecture where RPC requests contain both etype (event type) and rtype (response
type). The response rotates rtype to become its etype on the return trip. Both client and server can initiate RPC
requests, with clients using the value field for payloads and servers using data. RPC requests have the additional
args: any[] field for positional arguments, and kwargs: { [key: string]: any } for keyword arguments.
// Component-scoped Client Event
{ etype: "MOVABLE:{component-key}", value: {...} }
// Server Event
{ etype: "UPDATE", data: {...} }
// Server-to-client RPC event
{ etype: "CAMERA:{component-key}", rtype: "RESPONSE:{request_id}", kwargs: {...} }
// RPC Response
{ etype: "RESPONSE:{component-key}", value: {...}, ok: true, error: null }
// Hierarchical queue name in Zaku -- includes the args array.
{ etype: "WORKER_POOL:render:task-123", rtype: "{uuid}", args: [...], kwargs: {...} }VMP uses hierarchical naming with : separating components (e.g., MOVABLE:{component-key}:UPDATE). All event types follow screaming snake case with uppercase and underscores (e.g., SERVER_RPC, GRAB_RENDER). The colon separator enables natural prefix matching in Redis queries and component-scoped event handlers. Event types are namespaced by component type and instance ID (the component key).
Zaku relies on long-lived Redis queues for most of its async operations. In this case, routing is determined by the
name of the queue, which is usually set up prior to the request. Therefore, zaku RPC does not currently rely on etype,
but this will change when we introduce more complex object actions. For instance, a stateful worker that has multiple
life-cycle methods. In this case the etype will look like QUEUE_NAME:{method-name} where the $QUEUE_NAME is
postfixed with the worker ID or class name. Request payload will again be placed in the value field, while response
payloads use the data field (renamed from _results). We currently use _request_id for the response queue name. But
request types can be globally namespaced.
Example Usage:
Client side:
# Client makes RPC call
result = queue.rpc(seed=100, _timeout=5)
# Internally creates: {"_request_id": "rpc-{uuid}", "_args":[], "seed": 100}Worker side:
# Worker processes the job
with queue.pop() as job:
  topic = job.pop("_request_id")  # Extract response topic
  # Process job... then return the results as value
  queue.publish({"value": "good", "ok": True, "error": None}, topic=topic)The two implementations use fundamentally different message envelope designs, making unified serialization and RPC helpers challenging.
| Aspect | Vuer (Event-Based) | Zaku (Queue-Based) | Implications | 
|---|---|---|---|
| Routing Mechanism | Event types (flat namespace) e.g., "CLICK","RPC" | Queue names (hierarchical) e.g., "ZAKU_TEST:debug-queue-1" | Zaku supports namespace hierarchy, Vuer relies on flat event names | 
| Message Envelope | Nested structure: Client: {ts, etype, key?, value?}Server: {ts, etype, data} | Flat structure: {_request_id, _args?, ...kwargs} | Vuer separates server/client payloads with dedicated fields; Zaku mixes metadata and payload at same level | 
| Payload Structure | • data: Server → Client payload• value: Client → Server payload• Deep nesting supported | • All fields flattened at top level • No envelope/payload distinction • Metadata ( _request_id,_args) mixed with data | Vuer provides clean separation; Zaku optimized for Redis field-based search | 
| Serialization (Python) | Recursive multi-level: • serializer()walks nested structures• Calls ._serialize()on objects• Recursively processes lists/tuples • No special type handling | Flat single-level: • Payload.serialize()iterates top-level keys• ZData.encode()per value• Handles numpy, torch, PIL images • Does NOT recurse into nested dicts | Vuer can serialize arbitrary component trees; Zaku trades depth for numpy/torch support and Redis searchability | 
| Serialization (Transport) | msgpackr (TypeScript) msgpack (Python) Direct object packing | msgpack (Python only) Pre-processes with ZData encoding | Both use MessagePack, but Vuer relies on native msgpack features while Zaku wraps with custom type handling | 
| RPC Correlation | • Request: uuid+rtypefields• Response: Event type matches rtype• Example: rtype="RPC_RESPONSE@{uuid}" | • Request: _request_idfield• Response: Publish to topic _request_id• Example: "rpc-{uuid}" | Vuer uses typed events for correlation; Zaku uses pub/sub topics | 
| Type Safety | Event types define message schema TypeScript interfaces enforce structure | Queue names + payload keys define schema TypedDict for documentation only | Vuer has stronger client-side type checking; Zaku relies on runtime validation | 
Key Tensions:
- Envelope Design: Vuer's nested {etype, data/value}vs Zaku's flat{_request_id, **payload}prevents shared message handling
- Serialization Depth: Vuer's recursive serializer handles component trees; Zaku's single-level approach enables Redis field queries but cannot serialize deeply nested structures
- Special Type Handling: Zaku supports numpy/torch/PIL at top level; Vuer has no special handling (relies on user-side encoding)
Next Steps: Standardize serialization infrastructure before unifying message envelope formats. Current differences block generalized RPC helpers that work across both systems. Consider:
- Adopting Zaku's ZDataencoding in Vuer for numpy/torch support
- Defining a common envelope format that supports both flat (Redis-searchable) and nested (component-tree) use cases
For detailed implementation guides and examples, see:
- PROTOCOL_ANALYSIS.md- Technical deep dive
- CODE_EXAMPLES.md- Runnable code samples
- IMPLEMENTATION_GUIDE.md- Step-by-step implementation
- INDEX.md- Navigation and quick reference
MIT