Operations
All operations follow the pattern: dtype.verb.
Operations marked as LWW use Last-Write-Wins semantics based on Lamport time.
Additive operations accumulate regardless of order.
Schema-less Design
Vuer-RTC is schema-less—type information is embedded in the operation itself via the otype field.
No separate schema definition is required. The otype string (e.g., 'vector3.add') is parsed to extract:
- dtype: The data type (
vector3) - operation: The merge behavior (
add)
This enables dynamic properties without upfront schema definition.
Operation Format
All operations are flat (no nested objects) except for node operations that send node data. Common fields:
interface BaseOp {
otype: string; // Operation type: "dtype.verb"
key?: string; // Target node key ("." = root, default)
path?: string; // Property path on the node ("." = node root)
value?: unknown; // Value to apply (type depends on otype)
// Additional flat fields per operation type:
// index, from, to, alpha, separator, toPath, etc.
}Examples:
// Set position
{ otype: 'vector3.set', key: 'player-1', path: 'position', value: [1, 2, 3] }
// Blend color with alpha
{ otype: 'color.blend', key: 'sky', path: 'color', value: '#00ff00', alpha: 0.5 }
// Move array item
{ otype: 'array.move', key: 'item', path: 'tags', from: 0, to: 2 }
// Move node to new parent (flat)
{ otype: 'node.move', key: 'cube-1', to: 'group-1', toPath: 'children' }
// Node insert (nested value for node data)
{ otype: 'node.insert', key: '.', path: 'children', value: { key: 'cube-1', tag: 'Mesh' } }Number
| Type | Description | Example |
|---|---|---|
number.set | Set numeric value (LWW) | { otype: "number.set", key: "light-1", path: "intensity", value: 0.5 } |
number.add | Add to numeric value (additive) | { otype: "number.add", key: "player", path: "score", value: 10 } |
number.multiply | Multiply numeric value | { otype: "number.multiply", key: "sprite", path: "scale", value: 2 } |
number.min | Set to min(current, value) | { otype: "number.min", key: "enemy", path: "health", value: 0 } |
number.max | Set to max(current, value) | { otype: "number.max", key: "enemy", path: "health", value: 100 } |
Vector3
| Type | Description | Example |
|---|---|---|
vector3.set | Set position/scale (LWW) | { otype: "vector3.set", key: "cube", path: "position", value: [1, 2, 3] } |
vector3.add | Add to vector (additive) | { otype: "vector3.add", key: "player", path: "position", value: [0, 1, 0] } |
vector3.multiply | Component-wise multiply | { otype: "vector3.multiply", key: "mesh", path: "scale", value: [2, 2, 2] } |
vector3.applyEuler | Rotate by euler angles (radians) | { otype: "vector3.applyEuler", key: "arrow", path: "direction", value: [0, 1.57, 0], order: "YXZ" } |
vector3.applyQuaternion | Rotate by quaternion | { otype: "vector3.applyQuaternion", key: "arrow", path: "direction", value: [0, 0.7, 0, 0.7] } |
Euler
| Type | Description | Example |
|---|---|---|
euler.set | Set euler angles (LWW) | { otype: "euler.set", key: "camera", path: "rotation", value: [0, 1.57, 0] } |
euler.add | Add to euler angles (additive) | { otype: "euler.add", key: "turret", path: "rotation", value: [0.1, 0, 0] } |
Quaternion
| Type | Description | Example |
|---|---|---|
quaternion.set | Set rotation (LWW) | { otype: "quaternion.set", key: "bone", path: "rotation", value: [0, 0, 0, 1] } |
quaternion.multiply | Compose rotations | { otype: "quaternion.multiply", key: "joint", path: "rotation", value: [0, 0.7, 0, 0.7] } |
Color
| Type | Description | Example |
|---|---|---|
color.set | Set hex color (LWW) | { otype: "color.set", key: "material-1", path: "color", value: "#ff0000" } |
color.blend | Blend towards color | { otype: "color.blend", key: "sky", path: "color", value: "#00ff00", alpha: 0.5 } |
String
For move operations, to refers to the current index (before the move), not the post-move index. Negative indices count from the end.
"Hello World" → move [0:5] to index 6 → " WorldHello"
"Hello World" → move [0:5] to index -1 → " WorldHello"| Type | Description | Example |
|---|---|---|
string.set | Set string value (LWW) | { otype: "string.set", key: "label", path: "text", value: "Player 1" } |
string.concat | Append to string | { otype: "string.concat", key: "console", path: "log", value: "event", separator: "\n" } |
string.insert | Insert at index | { otype: "string.insert", key: "label", path: "text", value: "Hello ", index: 0 } |
string.cut | Remove substring | { otype: "string.cut", key: "label", path: "text", from: 0, to: 5 } |
string.replace | Replace substring | { otype: "string.replace", key: "label", path: "text", from: 0, to: 5, value: "Hi" } |
string.move | Move substring (to = current index) | { otype: "string.move", key: "label", path: "text", from: 0, to: 5, index: 10 } |
Boolean
| Type | Description | Example |
|---|---|---|
boolean.set | Set boolean (LWW) | { otype: "boolean.set", key: "mesh", path: "visible", value: true } |
boolean.or | OR operation | { otype: "boolean.or", key: "state", path: "dirty", value: true } |
boolean.and | AND operation | { otype: "boolean.and", key: "button", path: "enabled", value: false } |
boolean.xor | XOR operation (toggle) | { otype: "boolean.xor", key: "light", path: "on", value: true } |
Array
For move operations, to refers to the current index (before the move), not the post-move index. Negative indices count from the end.
["a", "b", "c", "d"] → move index 0 to index 2 → ["b", "c", "a", "d"]
["a", "b", "c", "d"] → move index 0 to index -2 → ["b", "c", "a", "d"]| Type | Description | Example |
|---|---|---|
array.set | Replace array (LWW) | { otype: "array.set", key: "item", path: "tags", value: ["a", "b"] } |
array.push | Append item | { otype: "array.push", key: "item", path: "tags", value: "new-tag" } |
array.insert | Insert at index | { otype: "array.insert", key: "item", path: "tags", value: "tag", index: 1 } |
array.remove | Remove by value or index | { otype: "array.remove", key: "item", path: "tags", value: "old-tag", index: 2 } |
array.move | Move item (to = current index) | { otype: "array.move", key: "item", path: "tags", from: 0, to: 2 } |
array.union | Add unique items | { otype: "array.union", key: "item", path: "tags", value: ["x", "y"] } |
Object
| Type | Description | Example |
|---|---|---|
object.set | Replace object (LWW) | { otype: "object.set", key: "entity", path: "metadata", value: { a: 1 } } |
object.update | Shallow merge | { otype: "object.update", key: "entity", path: ".", value: { visible: true } } |
object.remove | Remove key from object | { otype: "object.remove", key: "entity", path: "metadata.tempKey" } |
Node
Structural operations for the scene graph.
| Type | Description | Example |
|---|---|---|
node.insert | Insert child node under parent | { otype: "node.insert", key: ".", path: "children", value: { key: "cube-1", tag: "Mesh" } } |
node.remove | Delete node (tombstone) | { otype: "node.remove", key: "cube-1", path: "." } |
node.upsert | Insert or merge if exists | { otype: "node.upsert", key: "scene", path: "children", value: { key: "cube-1", position: [1,2,3] } } |
node.inset | Insert or set if exists | { otype: "node.inset", key: "scene", path: "children", value: { key: "cube-1", tag: "Mesh", visible: true } } |
node.move | Move node to new parent | { otype: "node.move", key: "cube-1", to: "group-1", toPath: "children" } |
Message Format
Operations are wrapped in a CRDTMessage that includes metadata for conflict resolution:
interface CRDTMessage {
id: string; // Unique message ID
sessionId: string; // Client session ID
clock: VectorClock; // Vector clock for causality
lamportTime: number; // Lamport time for total ordering
timestamp: number; // Wall-clock time
ops: Operation[]; // Batch of operations
}Euler Rotation Order
The vector3.applyEuler operation supports two optional parameters:
order: Rotation order -'XYZ'(default),'YXZ','ZXY','ZYX','YZX','XZY'intrinsic:true(default) for intrinsic rotation,falsefor extrinsic
Intrinsic vs. Extrinsic Rotations
- Intrinsic (default): Axes move with the body. Rotate around body's X axis, then around the new Y axis, then around the new Z axis.
- Extrinsic: Axes stay fixed in space. Rotate around fixed X, then fixed Y, then fixed Z.
Note: Intrinsic XYZ is equivalent to Extrinsic ZYX (reversed order).
// Intrinsic YXZ rotation (common for cameras/FPS)
{ otype: 'vector3.applyEuler', key: 'camera', path: 'direction', value: [pitch, yaw, roll], order: 'YXZ' }
// Extrinsic XYZ rotation (fixed-axis)
{ otype: 'vector3.applyEuler', key: 'arm', path: 'direction', value: [x, y, z], intrinsic: false }Common Use Cases
| Order | Use Case |
|---|---|
XYZ | Default, general 3D rotations |
YXZ | First-person cameras (yaw-pitch-roll) |
ZYX | Aircraft/aerospace conventions |
ZXY | Some motion capture systems |