GitHub

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-rtc

Quick 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") == 65

Operations

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}      # max

Vector3 / 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 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:

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-acknowledged

Duplicate 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

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:

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 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

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

To skip slow benchmarks:

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