GitHub

Examples

Practical examples demonstrating how to use Vuer-RTC for common scenarios.

Basic Scene Setup

import { createEmptyGraph, applyMessage } from '@vuer-ai/vuer-rtc';

// Initialize empty scene
let graph = createEmptyGraph();

// Create a cube mesh
graph = applyMessage(graph, {
  id: 'msg-1',
  sessionId: 'client-1',
  clock: { 'client-1': 1 },
  lamportTime: 1,
  timestamp: Date.now(),
  ops: [
    {
      otype: 'node.insert',
      key: 'cube',
      path: 'cube',
      value: { key: 'cube-uuid', tag: 'Mesh', name: 'My Cube' },
    },
    { otype: 'vector3.set', key: 'cube', path: 'position', value: [0, 1, 0] },
    { otype: 'color.set', key: 'cube', path: 'color', value: '#3b82f6' },
  ],
});

console.log(graph.nodes['cube']);
// { id: 'cube-uuid', key: 'cube', tag: 'Mesh', position: [0, 1, 0], color: '#3b82f6', ... }

Last-Write-Wins Conflict Resolution

When two clients edit the same property concurrently, the edit with the higher Lamport time wins, regardless of arrival order.

// Alice sets color to red (lamport: 5)
const aliceMsg = {
  sessionId: 'alice',
  lamportTime: 5,
  ops: [{ otype: 'color.set', key: 'cube', path: 'color', value: '#ff0000' }],
};

// Bob sets color to green (lamport: 7)
const bobMsg = {
  sessionId: 'bob',
  lamportTime: 7,
  ops: [{ otype: 'color.set', key: 'cube', path: 'color', value: '#00ff00' }],
};

// Even if Alice's message arrives AFTER Bob's:
graph = applyMessage(graph, bobMsg);  // color = green
graph = applyMessage(graph, aliceMsg); // color stays green (5 < 7)

// Bob wins because lamportTime 7 > 5

LWW ensures all clients converge to the same state, even when messages arrive out of order.

Additive Operations

Additive operations like vector3.add accumulate regardless of message order.

// Player starts at origin
graph = applyMessage(graph, {
  sessionId: 'init',
  lamportTime: 1,
  ops: [
    { otype: 'node.insert', key: 'player', path: 'player',
      value: { key: 'p-uuid', tag: 'Character', name: 'Player' } },
    { otype: 'vector3.set', key: 'player', path: 'position', value: [0, 0, 0] },
  ],
});

// Move forward by 5 units
const move1 = {
  lamportTime: 2,
  ops: [{ otype: 'vector3.add', key: 'player', path: 'position', value: [5, 0, 0] }],
};

// Move up by 2 units
const move2 = {
  lamportTime: 3,
  ops: [{ otype: 'vector3.add', key: 'player', path: 'position', value: [0, 2, 0] }],
};

// Order doesn't matter - result is always [5, 2, 0]
graph = applyMessage(graph, move2);
graph = applyMessage(graph, move1);
console.log(graph.nodes['player'].position); // [5, 2, 0]

Additive operations are commutative—apply them in any order and get the same result.

Journal-Based State

For full auditability, maintain both the computed graph and a journal of all messages.

interface State {
  graph: SceneGraph;
  journal: CRDTMessage[];
}

function processMessage(state: State, msg: CRDTMessage): State {
  return {
    journal: [...state.journal, msg],
    graph: applyMessage(state.graph, msg),
  };
}

// Reconstruct graph from journal at any point
function rebuildFromJournal(journal: CRDTMessage[]): SceneGraph {
  let graph = createEmptyGraph();
  for (const msg of journal) {
    graph = applyMessage(graph, msg);
  }
  return graph;
}

Parent-Child Relationships

Build hierarchical scene graphs using the parent option in node operations.

// Create a group node
graph = applyMessage(graph, {
  ops: [
    { otype: 'node.insert', key: 'enemies', path: 'enemies',
      value: { key: 'g-uuid', tag: 'Group', name: 'Enemies' } },
  ],
});

// Add children to the group using parent option
graph = applyMessage(graph, {
  ops: [
    { otype: 'node.insert', key: 'enemy-1', path: 'enemy-1',
      parent: 'enemies',  // Automatically added to parent's children
      value: { key: 'e1-uuid', tag: 'Mesh', name: 'Enemy 1' } },
    { otype: 'node.insert', key: 'enemy-2', path: 'enemy-2',
      parent: 'enemies',
      value: { key: 'e2-uuid', tag: 'Mesh', name: 'Enemy 2' } },
  ],
});

console.log(graph.nodes['enemies'].children);
// ['enemy-1', 'enemy-2']