GitHub

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.

Unified Schema

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

TypeDescriptionExample
number.setSet numeric value (LWW){ otype: "number.set", key: "light-1", path: "intensity", value: 0.5 }
number.addAdd to numeric value (additive){ otype: "number.add", key: "player", path: "score", value: 10 }
number.multiplyMultiply numeric value{ otype: "number.multiply", key: "sprite", path: "scale", value: 2 }
number.minSet to min(current, value){ otype: "number.min", key: "enemy", path: "health", value: 0 }
number.maxSet to max(current, value){ otype: "number.max", key: "enemy", path: "health", value: 100 }

Vector3

TypeDescriptionExample
vector3.setSet position/scale (LWW){ otype: "vector3.set", key: "cube", path: "position", value: [1, 2, 3] }
vector3.addAdd to vector (additive){ otype: "vector3.add", key: "player", path: "position", value: [0, 1, 0] }
vector3.multiplyComponent-wise multiply{ otype: "vector3.multiply", key: "mesh", path: "scale", value: [2, 2, 2] }
vector3.applyEulerRotate by euler angles (radians){ otype: "vector3.applyEuler", key: "arrow", path: "direction", value: [0, 1.57, 0], order: "YXZ" }
vector3.applyQuaternionRotate by quaternion{ otype: "vector3.applyQuaternion", key: "arrow", path: "direction", value: [0, 0.7, 0, 0.7] }

Euler

TypeDescriptionExample
euler.setSet euler angles (LWW){ otype: "euler.set", key: "camera", path: "rotation", value: [0, 1.57, 0] }
euler.addAdd to euler angles (additive){ otype: "euler.add", key: "turret", path: "rotation", value: [0.1, 0, 0] }

Quaternion

TypeDescriptionExample
quaternion.setSet rotation (LWW){ otype: "quaternion.set", key: "bone", path: "rotation", value: [0, 0, 0, 1] }
quaternion.multiplyCompose rotations{ otype: "quaternion.multiply", key: "joint", path: "rotation", value: [0, 0.7, 0, 0.7] }

Color

TypeDescriptionExample
color.setSet hex color (LWW){ otype: "color.set", key: "material-1", path: "color", value: "#ff0000" }
color.blendBlend 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"
TypeDescriptionExample
string.setSet string value (LWW){ otype: "string.set", key: "label", path: "text", value: "Player 1" }
string.concatAppend to string{ otype: "string.concat", key: "console", path: "log", value: "event", separator: "\n" }
string.insertInsert at index{ otype: "string.insert", key: "label", path: "text", value: "Hello ", index: 0 }
string.cutRemove substring{ otype: "string.cut", key: "label", path: "text", from: 0, to: 5 }
string.replaceReplace substring{ otype: "string.replace", key: "label", path: "text", from: 0, to: 5, value: "Hi" }
string.moveMove substring (to = current index){ otype: "string.move", key: "label", path: "text", from: 0, to: 5, index: 10 }

Boolean

TypeDescriptionExample
boolean.setSet boolean (LWW){ otype: "boolean.set", key: "mesh", path: "visible", value: true }
boolean.orOR operation{ otype: "boolean.or", key: "state", path: "dirty", value: true }
boolean.andAND operation{ otype: "boolean.and", key: "button", path: "enabled", value: false }
boolean.xorXOR 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"]
TypeDescriptionExample
array.setReplace array (LWW){ otype: "array.set", key: "item", path: "tags", value: ["a", "b"] }
array.pushAppend item{ otype: "array.push", key: "item", path: "tags", value: "new-tag" }
array.insertInsert at index{ otype: "array.insert", key: "item", path: "tags", value: "tag", index: 1 }
array.removeRemove by value or index{ otype: "array.remove", key: "item", path: "tags", value: "old-tag", index: 2 }
array.moveMove item (to = current index){ otype: "array.move", key: "item", path: "tags", from: 0, to: 2 }
array.unionAdd unique items{ otype: "array.union", key: "item", path: "tags", value: ["x", "y"] }

Object

TypeDescriptionExample
object.setReplace object (LWW){ otype: "object.set", key: "entity", path: "metadata", value: { a: 1 } }
object.updateShallow merge{ otype: "object.update", key: "entity", path: ".", value: { visible: true } }
object.removeRemove key from object{ otype: "object.remove", key: "entity", path: "metadata.tempKey" }

Node

Structural operations for the scene graph.

TypeDescriptionExample
node.insertInsert child node under parent{ otype: "node.insert", key: ".", path: "children", value: { key: "cube-1", tag: "Mesh" } }
node.removeDelete node (tombstone){ otype: "node.remove", key: "cube-1", path: "." }
node.upsertInsert or merge if exists{ otype: "node.upsert", key: "scene", path: "children", value: { key: "cube-1", position: [1,2,3] } }
node.insetInsert or set if exists{ otype: "node.inset", key: "scene", path: "children", value: { key: "cube-1", tag: "Mesh", visible: true } }
node.moveMove 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, false for 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

OrderUse Case
XYZDefault, general 3D rotations
YXZFirst-person cameras (yaw-pitch-roll)
ZYXAircraft/aerospace conventions
ZXYSome motion capture systems