Vuer RTC

Python Client

A Python port of the TypeScript @vuer-ai/vuer-rtc library. Multiple clients can concurrently edit a shared scene graph and all changes converge automatically.

Install

bash
pip install vuer-rtc

Quick Start

python
from vuer_rtc import create_graph

# Create a client store
store = create_graph("client-1", on_send=lambda msg: send_to_server(msg))

# Build a scene
store.edit({
    "ot": "node.insert",
    "key": "",            # parent key ("" = root)
    "path": "children",
    "value": {"key": "scene", "tag": "Scene", "name": "My Scene"},
})
store.edit({
    "ot": "node.insert",
    "key": "scene",
    "path": "children",
    "value": {"key": "cube", "tag": "Mesh", "position": [0, 1, 0], "opacity": 1.0},
})
store.commit("create scene")

# Edit properties
store.edit({"ot": "vector3.set", "key": "cube", "path": "position", "value": [3, 2, 1]})
store.edit({"ot": "number.set",  "key": "cube", "path": "opacity",  "value": 0.5})
store.commit("move and fade cube")

# Read state
graph = store.get_state().graph
print(graph.nodes["cube"].get_property("position"))  # [3, 2, 1]

Two-Client Example

python
from vuer_rtc import create_graph

messages_a, messages_b = [], []

store_a = create_graph("alice", on_send=lambda msg: messages_a.append(msg))
store_b = create_graph("bob",   on_send=lambda msg: messages_b.append(msg))

# Alice creates a node
store_a.edit({
    "ot": "node.insert",
    "key": "",
    "path": "children",
    "value": {"key": "obj", "tag": "Mesh", "health": 100},
})
msg = store_a.commit("create obj")

# Bob receives Alice's message
store_b.receive(msg)

# Both edit concurrently
store_a.edit({"ot": "number.add", "key": "obj", "path": "health", "value": -25})
msg_a = store_a.commit("damage")

store_b.edit({"ot": "number.add", "key": "obj", "path": "health", "value": -10})
msg_b = store_b.commit("poison")

# Exchange messages
store_a.receive(msg_b)
store_b.receive(msg_a)

# Both converge: 100 + (-25) + (-10) = 65
assert store_a.get_state().graph.nodes["obj"].get_property("health") == 65
assert store_b.get_state().graph.nodes["obj"].get_property("health") == 65

Operations

Every operation is a dict with ot, key, path, and typically value. The Python client supports all the same operations as the TypeScript client.

Number

python
{"ot": "number.set",      "key": "n", "path": "score",    "value": 42}     # LWW
{"ot": "number.add",      "key": "n", "path": "counter",  "value": 1}      # additive
{"ot": "number.multiply", "key": "n", "path": "scale",    "value": 2}      # multiplicative
{"ot": "number.min",      "key": "n", "path": "cooldown", "value": 5}      # min
{"ot": "number.max",      "key": "n", "path": "health",   "value": 0}      # max

Vector3 / Quaternion / Euler

python
{"ot": "vector3.set", "key": "n", "path": "position", "value": [1, 2, 3]}
{"ot": "vector3.add", "key": "n", "path": "velocity", "value": [0, 1, 0]}

{"ot": "quaternion.set",      "key": "n", "path": "rotation", "value": [0, 0, 0, 1]}
{"ot": "quaternion.multiply", "key": "n", "path": "rotation", "value": [0, 0.707, 0, 0.707]}

{"ot": "euler.set", "key": "n", "path": "rotation", "value": [0, 1.57, 0]}
{"ot": "euler.add", "key": "n", "path": "rotation", "value": [0, 0.1, 0]}

String / Boolean / Color

python
{"ot": "string.set",   "key": "n", "path": "name",    "value": "Cube"}
{"ot": "string.concat", "key": "n", "path": "log",     "value": " line2", "separator": "\n"}

{"ot": "boolean.set", "key": "n", "path": "visible", "value": True}
{"ot": "boolean.or",  "key": "n", "path": "dirty",   "value": True}
{"ot": "boolean.and", "key": "n", "path": "locked",  "value": False}

{"ot": "color.set",   "key": "n", "path": "color", "value": "#ff0000"}
{"ot": "color.blend",  "key": "n", "path": "color", "value": "#0000ff", "alpha": 0.5}

Array / Object

python
{"ot": "array.set",    "key": "n", "path": "items", "value": [1, 2, 3]}
{"ot": "array.push",   "key": "n", "path": "items", "value": 4}
{"ot": "array.remove",  "key": "n", "path": "items", "value": 2}
{"ot": "array.union",   "key": "n", "path": "tags",  "value": ["a", "b"]}

{"ot": "object.set",   "key": "n", "path": "config", "value": {"debug": True}}
{"ot": "object.merge",  "key": "n", "path": "config", "value": {"verbose": True}}

Node (Scene Graph Structure)

python
# Insert a child node
{"ot": "node.insert", "key": "parent", "path": "children",
 "value": {"key": "child", "tag": "Mesh", "name": "Child"}}

# Remove (soft-delete / tombstone)
{"ot": "node.remove", "key": "parent", "path": "children", "value": "child"}

# Move a node to a new parent
{"ot": "node.move", "key": "old-parent", "path": "children",
 "value": {"nodeKey": "child", "newParent": "new-parent"}}

Text (Collaborative CRDT)

python
# Initialize a text property
{"ot": "text.init",    "key": "doc", "path": "content"}

# Insert text at a position. `value` is an [anchor, content] tuple; use None for
# the anchor on a position-based local insert (the CRDT computes it).
{"ot": "text.insert",  "key": "doc", "path": "content", "position": 0, "value": [None, "Hello"]}

# Delete a range
{"ot": "text.delete",  "key": "doc", "path": "content", "position": 0, "length": 5}

# Atomic delete + insert (for select-and-type)
{"ot": "text.replace", "key": "doc", "path": "content", "position": 0, "length": 5, "value": [None, "Hi"]}

Important: Use text.replace instead of separate text.delete + text.insert when replacing a selection. The edit buffer deduplicates by key:path, so a delete followed by an insert on the same key and path will lose the delete.

Undo / Redo

python
store.edit({"ot": "number.set", "key": "n", "path": "x", "value": 10})
store.commit("set x")

store.undo()  # x reverts to previous value
store.redo()  # x = 10 again

Undo/redo is journal-based. Each undo() marks a journal entry as deleted and replays the remaining entries. This works correctly across concurrent edits from multiple clients.

Edit Buffer

Edits are buffered until commit(). Additive operations on the same key:path are merged automatically:

python
store.edit({"ot": "vector3.add", "key": "n", "path": "pos", "value": [1, 0, 0]})
store.edit({"ot": "vector3.add", "key": "n", "path": "pos", "value": [0, 1, 0]})
store.edit({"ot": "vector3.add", "key": "n", "path": "pos", "value": [0, 0, 1]})

# Only one op in the buffer: value = [1, 1, 1]
assert len(store.get_state().edits.ops) == 1

store.commit("combined move")

Use store.cancel() to discard uncommitted edits and revert to the pre-edit graph.

Receiving Remote Messages

python
store.receive(msg)      # apply a CRDTMessage from the server
store.ack(msg_id)       # mark one of our messages as server-acknowledged

Duplicate messages are automatically ignored (idempotent).

Retry and Compaction

python
from vuer_rtc import get_unacked_messages

# Retry unacknowledged messages (e.g. after reconnect)
for msg in get_unacked_messages(store.get_state()):
    send_to_server(msg)

# Compact acknowledged journal entries into a snapshot
store.compact()

API Reference

GraphStore

MethodDescription
edit(op)Add operation to edit buffer (optimistic apply)
commit(description?)Commit edits as a single CRDTMessage
cancel()Discard uncommitted edits
receive(msg)Process incoming remote CRDTMessage
ack(msg_id)Mark a journal entry as server-acknowledged
undo()Undo last committed message from this session
redo()Redo last undone message from this session
compact()Bake acknowledged entries into snapshot
get_state()Return current ClientState

Pure Functions

For advanced use cases, the bare state-transition functions are also exported:

python
from vuer_rtc import (
    create_initial_state,
    on_edit, commit_edits, cancel_edits,
    on_server_ack, on_remote_message,
    undo, redo, compact, rebuild_graph,
)

Low-Level Operations

python
from vuer_rtc import (
    apply_operation,    # Apply a single op to a SceneGraph (mutates in place)
    apply_message,      # Apply a CRDTMessage (returns new graph)
    apply_message_mut,  # Apply a CRDTMessage (mutates in place)
    apply_messages,     # Apply multiple messages
    create_empty_graph, # Create an empty SceneGraph
)

Conflict Resolution

Merge StrategyOperation Types
Last-Write-Wins (LWW)*.set — highest Lamport timestamp wins
Additivenumber.add, vector3.add, quaternion.multiply — values accumulate
Commutativeboolean.or/boolean.and, number.min/number.max, array.union
Deep mergeobject.merge — recursive per-key merge
CRDT texttext.insert/text.delete/text.replace — RGA/YATA algorithm

All strategies are deterministic: given the same set of operations (in any order), every client converges to the same state.

Running Tests

bash
pip install -e ".[test]"
pytest tests/ -x -q

To skip slow benchmarks:

bash
pytest tests/ -x -q -m "not slow"

Lossless checkpoints (Python 0.0.2)

Keep the server snapshot's textRopes field when constructing Snapshot. Both createGraph(initial_snapshot=...) and GraphStore.fromServer(...) hydrate this metadata automatically; custom consumers can call hydrateTextSnapshot(snapshot). The Python graph retains its existing _textCrdt.<path> rope plus visible string representation. TypeScript camelCase rope fields and deleted anchors are supported, and fresh local inserts use the local agent ID.

The raw MessagePack codec forwards additive sync-check, sync-status, and checkpoint metadata unchanged. Python does not automatically run a checksum monitor or manage held drafts. Compare note-body hashes only at equal committed clocks with no pending edits; legacy strings cannot recover discarded character identities.