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 > 5LWW 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']