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
pip install vuer-rtcQuick Start
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({
"otype": "node.insert",
"key": "", # parent key ("" = root)
"path": "children",
"value": {"key": "scene", "tag": "Scene", "name": "My Scene"},
})
store.edit({
"otype": "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({"otype": "vector3.set", "key": "cube", "path": "position", "value": [3, 2, 1]})
store.edit({"otype": "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
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({
"otype": "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({"otype": "number.add", "key": "obj", "path": "health", "value": -25})
msg_a = store_a.commit("damage")
store_b.edit({"otype": "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") == 65Operations
Every operation is a dict with otype, key, path, and typically value. The Python client supports all the same operations as the TypeScript client.
Number
{"otype": "number.set", "key": "n", "path": "score", "value": 42} # LWW
{"otype": "number.add", "key": "n", "path": "counter", "value": 1} # additive
{"otype": "number.multiply", "key": "n", "path": "scale", "value": 2} # multiplicative
{"otype": "number.min", "key": "n", "path": "cooldown", "value": 5} # min
{"otype": "number.max", "key": "n", "path": "health", "value": 0} # maxVector3 / Quaternion / Euler
{"otype": "vector3.set", "key": "n", "path": "position", "value": [1, 2, 3]}
{"otype": "vector3.add", "key": "n", "path": "velocity", "value": [0, 1, 0]}
{"otype": "quaternion.set", "key": "n", "path": "rotation", "value": [0, 0, 0, 1]}
{"otype": "quaternion.multiply", "key": "n", "path": "rotation", "value": [0, 0.707, 0, 0.707]}
{"otype": "euler.set", "key": "n", "path": "rotation", "value": [0, 1.57, 0]}
{"otype": "euler.add", "key": "n", "path": "rotation", "value": [0, 0.1, 0]}String / Boolean / Color
{"otype": "string.set", "key": "n", "path": "name", "value": "Cube"}
{"otype": "string.concat", "key": "n", "path": "log", "value": " line2", "separator": "\n"}
{"otype": "boolean.set", "key": "n", "path": "visible", "value": True}
{"otype": "boolean.or", "key": "n", "path": "dirty", "value": True}
{"otype": "boolean.and", "key": "n", "path": "locked", "value": False}
{"otype": "color.set", "key": "n", "path": "color", "value": "#ff0000"}
{"otype": "color.blend", "key": "n", "path": "color", "value": "#0000ff", "alpha": 0.5}Array / Object
{"otype": "array.set", "key": "n", "path": "items", "value": [1, 2, 3]}
{"otype": "array.push", "key": "n", "path": "items", "value": 4}
{"otype": "array.remove", "key": "n", "path": "items", "value": 2}
{"otype": "array.union", "key": "n", "path": "tags", "value": ["a", "b"]}
{"otype": "object.set", "key": "n", "path": "config", "value": {"debug": True}}
{"otype": "object.merge", "key": "n", "path": "config", "value": {"verbose": True}}Node (Scene Graph Structure)
# Insert a child node
{"otype": "node.insert", "key": "parent", "path": "children",
"value": {"key": "child", "tag": "Mesh", "name": "Child"}}
# Remove (soft-delete / tombstone)
{"otype": "node.remove", "key": "parent", "path": "children", "value": "child"}
# Move a node to a new parent
{"otype": "node.move", "key": "old-parent", "path": "children",
"value": {"nodeKey": "child", "newParent": "new-parent"}}Text (Collaborative CRDT)
# Initialize a text property
{"otype": "text.init", "key": "doc", "path": "content"}
# Insert text at a position
{"otype": "text.insert", "key": "doc", "path": "content", "position": 0, "value": "Hello"}
# Delete a range
{"otype": "text.delete", "key": "doc", "path": "content", "position": 0, "length": 5}
# Atomic delete + insert (for select-and-type)
{"otype": "text.replace", "key": "doc", "path": "content", "position": 0, "length": 5, "value": "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
store.edit({"otype": "number.set", "key": "n", "path": "x", "value": 10})
store.commit("set x")
store.undo() # x reverts to previous value
store.redo() # x = 10 againUndo/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:
store.edit({"otype": "vector3.add", "key": "n", "path": "pos", "value": [1, 0, 0]})
store.edit({"otype": "vector3.add", "key": "n", "path": "pos", "value": [0, 1, 0]})
store.edit({"otype": "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
store.receive(msg) # apply a CRDTMessage from the server
store.ack(msg_id) # mark one of our messages as server-acknowledgedDuplicate messages are automatically ignored (idempotent).
Retry and Compaction
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
| Method | Description |
|---|---|
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:
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
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 Strategy | Operation Types |
|---|---|
| Last-Write-Wins (LWW) | *.set — highest Lamport timestamp wins |
| Additive | number.add, vector3.add, quaternion.multiply — values accumulate |
| Commutative | boolean.or/boolean.and, number.min/number.max, array.union |
| Deep merge | object.merge — recursive per-key merge |
| CRDT text | text.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
pip install -e ".[test]"
pytest tests/ -x -qTo skip slow benchmarks:
pytest tests/ -x -q -m "not slow"