@vuer-ai/vuer-rtc-server 0.2.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/.env +1 -1
  2. package/README.md +56 -0
  3. package/dist/archive/ArchivalService.js +1 -1
  4. package/dist/archive/ArchivalService.js.map +1 -1
  5. package/dist/broker/InMemoryBroker.d.ts +2 -2
  6. package/dist/broker/InMemoryBroker.d.ts.map +1 -1
  7. package/dist/broker/InMemoryBroker.js +4 -4
  8. package/dist/broker/InMemoryBroker.js.map +1 -1
  9. package/dist/broker/types.d.ts +3 -3
  10. package/dist/broker/types.d.ts.map +1 -1
  11. package/dist/journal/CoalescingService.d.ts.map +1 -1
  12. package/dist/journal/CoalescingService.js +18 -208
  13. package/dist/journal/CoalescingService.js.map +1 -1
  14. package/dist/journal/GraphJournalService.d.ts +127 -0
  15. package/dist/journal/GraphJournalService.d.ts.map +1 -0
  16. package/dist/journal/GraphJournalService.js +491 -0
  17. package/dist/journal/GraphJournalService.js.map +1 -0
  18. package/dist/journal/JournalRLE.d.ts +2 -2
  19. package/dist/journal/JournalRLE.js +14 -14
  20. package/dist/journal/JournalRLE.js.map +1 -1
  21. package/dist/journal/JournalRepository.js +7 -7
  22. package/dist/journal/JournalRepository.js.map +1 -1
  23. package/dist/journal/JournalService.d.ts.map +1 -1
  24. package/dist/journal/JournalService.js +6 -40
  25. package/dist/journal/JournalService.js.map +1 -1
  26. package/dist/journal/RLECompression.d.ts +9 -9
  27. package/dist/journal/RLECompression.d.ts.map +1 -1
  28. package/dist/journal/RLECompression.js +22 -22
  29. package/dist/journal/RLECompression.js.map +1 -1
  30. package/dist/journal/TextJournalService.d.ts +98 -0
  31. package/dist/journal/TextJournalService.d.ts.map +1 -0
  32. package/dist/journal/TextJournalService.js +401 -0
  33. package/dist/journal/TextJournalService.js.map +1 -0
  34. package/dist/journal/index.d.ts +3 -1
  35. package/dist/journal/index.d.ts.map +1 -1
  36. package/dist/journal/index.js +4 -1
  37. package/dist/journal/index.js.map +1 -1
  38. package/dist/journal/rle-demo.js +11 -11
  39. package/dist/journal/rle-demo.js.map +1 -1
  40. package/dist/serve.d.ts +29 -11
  41. package/dist/serve.d.ts.map +1 -1
  42. package/dist/serve.js +558 -93
  43. package/dist/serve.js.map +1 -1
  44. package/dist/transport/RTCServer.d.ts +2 -2
  45. package/dist/transport/RTCServer.d.ts.map +1 -1
  46. package/dist/transport/RTCServer.js +22 -22
  47. package/dist/transport/RTCServer.js.map +1 -1
  48. package/docs/API.md +642 -0
  49. package/examples/compression-example.ts +3 -3
  50. package/package.json +2 -2
  51. package/prisma/schema.prisma +124 -6
  52. package/src/archive/ArchivalService.ts +1 -1
  53. package/src/broker/InMemoryBroker.ts +4 -4
  54. package/src/broker/types.ts +3 -3
  55. package/src/journal/CoalescingService.ts +18 -235
  56. package/src/journal/{JournalService.ts → GraphJournalService.ts} +34 -74
  57. package/src/journal/JournalRLE.ts +15 -15
  58. package/src/journal/JournalRepository.ts +7 -7
  59. package/src/journal/RLECompression.ts +24 -24
  60. package/src/journal/TextJournalService.ts +483 -0
  61. package/src/journal/index.ts +10 -2
  62. package/src/journal/rle-demo.ts +11 -11
  63. package/src/serve.ts +598 -94
  64. package/src/transport/RTCServer.ts +23 -23
  65. package/tests/benchmark/journal-optimization-benchmark.test.ts +14 -14
  66. package/tests/compression/compression.test.ts +8 -8
  67. package/tests/demo.ts +88 -88
  68. package/tests/e2e/convergence.test.ts +9 -9
  69. package/tests/e2e/helpers/assertions.ts +22 -0
  70. package/tests/e2e/helpers/createTestServer.ts +4 -4
  71. package/tests/e2e/latency.test.ts +47 -41
  72. package/tests/e2e/packet-loss.test.ts +6 -6
  73. package/tests/e2e/relay.test.ts +9 -9
  74. package/tests/e2e/sync-perf.test.ts +5 -5
  75. package/tests/e2e/sync-reconciliation.test.ts +6 -6
  76. package/tests/e2e/text-sync.test.ts +14 -14
  77. package/tests/e2e/tombstone-convergence.test.ts +22 -22
  78. package/tests/fixtures/array-ops.jsonl +6 -6
  79. package/tests/fixtures/boolean-ops.jsonl +6 -6
  80. package/tests/fixtures/color-ops.jsonl +4 -4
  81. package/tests/fixtures/edit-buffer.jsonl +3 -3
  82. package/tests/fixtures/messages.jsonl +4 -4
  83. package/tests/fixtures/node-ops.jsonl +6 -6
  84. package/tests/fixtures/number-ops.jsonl +7 -7
  85. package/tests/fixtures/object-ops.jsonl +4 -4
  86. package/tests/fixtures/operations.jsonl +7 -7
  87. package/tests/fixtures/string-ops.jsonl +4 -4
  88. package/tests/fixtures/undo-redo.jsonl +3 -3
  89. package/tests/fixtures/vector-ops.jsonl +9 -9
  90. package/tests/integration/repositories.test.ts +8 -9
  91. package/tests/journal/compaction-load-bug.test.ts +31 -31
  92. package/tests/journal/compaction.test.ts +26 -26
  93. package/tests/journal/journal-rle.test.ts +38 -38
  94. package/tests/journal/journal-service.test.ts +13 -13
  95. package/tests/journal/lww-ordering-bug.test.ts +39 -39
  96. package/tests/journal/rle-compression.test.ts +71 -71
  97. package/tests/journal/text-coalescing.test.ts +34 -34
  98. package/tests/test-data/datatypes.ts +85 -85
  99. package/tests/test-data/operations-example.ts +62 -62
  100. package/tests/test-data/scene-example.ts +11 -11
  101. package/tests/unit/operations.test.ts +7 -7
  102. package/tests/unit/s3-compression.test.ts +5 -3
  103. package/tests/unit/vectorClock.test.ts +2 -2
  104. package/tests/journal/multi-session-coalescing.test.ts +0 -871
package/tests/demo.ts CHANGED
@@ -15,22 +15,22 @@ type VectorClock = Record<string, number>;
15
15
 
16
16
  interface CRDTMessage {
17
17
  id: string;
18
- sessionId: string;
18
+ client: string;
19
19
  clock: VectorClock;
20
- lamportTime: number;
21
- timestamp: number;
20
+ lt: number;
21
+ ts: number;
22
22
  ops: Operation[];
23
23
  }
24
24
 
25
25
  interface BaseOp {
26
26
  key: string;
27
- otype: string;
27
+ ot: string;
28
28
  path: string;
29
29
  }
30
30
 
31
31
  interface NodeInsertOp extends BaseOp {
32
32
  key: string;
33
- otype: 'node.insert';
33
+ ot: 'node.insert';
34
34
  path: string;
35
35
  value: {
36
36
  key: string;
@@ -42,62 +42,62 @@ interface NodeInsertOp extends BaseOp {
42
42
 
43
43
  interface NodeRemoveOp extends BaseOp {
44
44
  key: string;
45
- otype: 'node.remove';
45
+ ot: 'node.remove';
46
46
  path: string;
47
47
  }
48
48
 
49
49
  interface NumberSetOp extends BaseOp {
50
50
  key: string;
51
- otype: 'number.set';
51
+ ot: 'number.set';
52
52
  path: string;
53
53
  value: number;
54
54
  }
55
55
 
56
56
  interface NumberAddOp extends BaseOp {
57
57
  key: string;
58
- otype: 'number.add';
58
+ ot: 'number.add';
59
59
  path: string;
60
60
  value: number;
61
61
  }
62
62
 
63
63
  interface Vector3SetOp extends BaseOp {
64
64
  key: string;
65
- otype: 'vector3.set';
65
+ ot: 'vector3.set';
66
66
  path: string;
67
67
  value: [number, number, number];
68
68
  }
69
69
 
70
70
  interface Vector3AddOp extends BaseOp {
71
71
  key: string;
72
- otype: 'vector3.add';
72
+ ot: 'vector3.add';
73
73
  path: string;
74
74
  value: [number, number, number];
75
75
  }
76
76
 
77
77
  interface ColorSetOp extends BaseOp {
78
78
  key: string;
79
- otype: 'color.set';
79
+ ot: 'color.set';
80
80
  path: string;
81
81
  value: string;
82
82
  }
83
83
 
84
84
  interface ArraySetOp extends BaseOp {
85
85
  key: string;
86
- otype: 'array.set';
86
+ ot: 'array.set';
87
87
  path: string;
88
88
  value: any[];
89
89
  }
90
90
 
91
91
  interface ArrayPushOp extends BaseOp {
92
92
  key: string;
93
- otype: 'array.push';
93
+ ot: 'array.push';
94
94
  path: string;
95
95
  value: any;
96
96
  }
97
97
 
98
98
  interface ArrayRemoveOp extends BaseOp {
99
99
  key: string;
100
- otype: 'array.remove';
100
+ ot: 'array.remove';
101
101
  path: string;
102
102
  value: any;
103
103
  }
@@ -125,16 +125,16 @@ console.log('\n📦 Step 1: Create Scene\n');
125
125
  const msg1: CRDTMessage = {
126
126
  // === CRDT Wrapper (envelope) ===
127
127
  id: 'msg-001',
128
- sessionId: 'session-server',
128
+ client: 'session-server',
129
129
  clock: { 'session-server': 1 },
130
- lamportTime: 1,
131
- timestamp: Date.now() / 1000,
130
+ lt: 1,
131
+ ts: Date.now() / 1000,
132
132
 
133
133
  // === Operations (batch) ===
134
134
  ops: [
135
135
  {
136
136
  key: 'scene',
137
- otype: 'node.insert',
137
+ ot: 'node.insert',
138
138
  path: 'scene',
139
139
  value: {
140
140
  key: 'uuid-scene-001',
@@ -150,11 +150,11 @@ const msg1: CRDTMessage = {
150
150
  };
151
151
 
152
152
  console.log('Message 1:');
153
- console.log(' Envelope: id=%s, session=%s, lamport=%d', msg1.id, msg1.sessionId, msg1.lamportTime);
153
+ console.log(' Envelope: id=%s, session=%s, lamport=%d', msg1.id, msg1.client, msg1.lt);
154
154
  console.log(' Operations: %d ops', msg1.ops.length);
155
155
  const op1 = msg1.ops[0];
156
- if (op1.otype === 'node.insert') {
157
- console.log(' ✓ %s: %s (tag=%s)', op1.otype, op1.key, op1.value.tag);
156
+ if (op1.ot === 'node.insert') {
157
+ console.log(' ✓ %s: %s (tag=%s)', op1.ot, op1.key, op1.value.tag);
158
158
  }
159
159
 
160
160
  // ========================================
@@ -164,15 +164,15 @@ console.log('\n📦 Step 2: Batch Insert - Multiple Nodes\n');
164
164
 
165
165
  const msg2: CRDTMessage = {
166
166
  id: 'msg-002',
167
- sessionId: 'session-alice',
167
+ client: 'session-alice',
168
168
  clock: { 'session-alice': 1 },
169
- lamportTime: 2,
170
- timestamp: Date.now() / 1000,
169
+ lt: 2,
170
+ ts: Date.now() / 1000,
171
171
  ops: [
172
172
  // Insert cube
173
173
  {
174
174
  key: 'cube-1',
175
- otype: 'node.insert',
175
+ ot: 'node.insert',
176
176
  path: 'cube-1',
177
177
  value: {
178
178
  key: 'uuid-cube-001',
@@ -187,7 +187,7 @@ const msg2: CRDTMessage = {
187
187
  // Insert sphere
188
188
  {
189
189
  key: 'sphere-1',
190
- otype: 'node.insert',
190
+ ot: 'node.insert',
191
191
  path: 'sphere-1',
192
192
  value: {
193
193
  key: 'uuid-sphere-001',
@@ -202,7 +202,7 @@ const msg2: CRDTMessage = {
202
202
  // Add both to scene's children
203
203
  {
204
204
  key: 'scene',
205
- otype: 'array.set',
205
+ ot: 'array.set',
206
206
  path: 'children',
207
207
  value: ['cube-1', 'sphere-1'],
208
208
  },
@@ -210,13 +210,13 @@ const msg2: CRDTMessage = {
210
210
  };
211
211
 
212
212
  console.log('Message 2 (BATCH):');
213
- console.log(' Envelope: id=%s, session=%s, lamport=%d', msg2.id, msg2.sessionId, msg2.lamportTime);
213
+ console.log(' Envelope: id=%s, session=%s, lamport=%d', msg2.id, msg2.client, msg2.lt);
214
214
  console.log(' Operations: %d ops (BATCHED!)', msg2.ops.length);
215
215
  msg2.ops.forEach((op) => {
216
- if (op.otype === 'node.insert') {
217
- console.log(' ✓ %s: %s (tag=%s)', op.otype, op.key, op.value.tag);
216
+ if (op.ot === 'node.insert') {
217
+ console.log(' ✓ %s: %s (tag=%s)', op.ot, op.key, op.value.tag);
218
218
  } else {
219
- console.log(' ✓ %s: %s.%s', op.otype, op.key, op.path);
219
+ console.log(' ✓ %s: %s.%s', op.ot, op.key, op.path);
220
220
  }
221
221
  });
222
222
 
@@ -227,14 +227,14 @@ console.log('\n🎯 Step 3: Additive Transform (Drag)\n');
227
227
 
228
228
  const msg3: CRDTMessage = {
229
229
  id: 'msg-003',
230
- sessionId: 'session-alice',
230
+ client: 'session-alice',
231
231
  clock: { 'session-alice': 2 },
232
- lamportTime: 3,
233
- timestamp: Date.now() / 1000,
232
+ lt: 3,
233
+ ts: Date.now() / 1000,
234
234
  ops: [
235
235
  {
236
236
  key: 'cube-1',
237
- otype: 'vector3.add', // Additive!
237
+ ot: 'vector3.add', // Additive!
238
238
  path: 'transform.position',
239
239
  value: [5, 0, 0], // Drag by +5 on X
240
240
  },
@@ -243,7 +243,7 @@ const msg3: CRDTMessage = {
243
243
 
244
244
  console.log('Message 3 (Additive):');
245
245
  const op3 = msg3.ops[0] as Vector3AddOp;
246
- console.log(' ✓ %s: %s.%s += %s', op3.otype, op3.key, op3.path, JSON.stringify(op3.value));
246
+ console.log(' ✓ %s: %s.%s += %s', op3.ot, op3.key, op3.path, JSON.stringify(op3.value));
247
247
  console.log(' → Relative movement (position.x += 5)');
248
248
 
249
249
  // ========================================
@@ -253,14 +253,14 @@ console.log('\n🎯 Step 4: Absolute Transform (Set)\n');
253
253
 
254
254
  const msg4: CRDTMessage = {
255
255
  id: 'msg-004',
256
- sessionId: 'session-bob',
256
+ client: 'session-bob',
257
257
  clock: { 'session-bob': 1 },
258
- lamportTime: 4,
259
- timestamp: Date.now() / 1000,
258
+ lt: 4,
259
+ ts: Date.now() / 1000,
260
260
  ops: [
261
261
  {
262
262
  key: 'sphere-1',
263
- otype: 'vector3.set', // Absolute!
263
+ ot: 'vector3.set', // Absolute!
264
264
  path: 'transform.position',
265
265
  value: [0, 5, 0], // Set to the exact position
266
266
  },
@@ -269,7 +269,7 @@ const msg4: CRDTMessage = {
269
269
 
270
270
  console.log('Message 4 (Absolute):');
271
271
  const op4 = msg4.ops[0] as Vector3SetOp;
272
- console.log(' ✓ %s: %s.%s = %s', op4.otype, op4.key, op4.path, JSON.stringify(op4.value));
272
+ console.log(' ✓ %s: %s.%s = %s', op4.ot, op4.key, op4.path, JSON.stringify(op4.value));
273
273
  console.log(' → Absolute position (position = [0, 5, 0])');
274
274
 
275
275
  // ========================================
@@ -279,26 +279,26 @@ console.log('\n🔄 Step 5: Batch Update - Same Node\n');
279
279
 
280
280
  const msg5: CRDTMessage = {
281
281
  id: 'msg-005',
282
- sessionId: 'session-alice',
282
+ client: 'session-alice',
283
283
  clock: { 'session-alice': 3 },
284
- lamportTime: 5,
285
- timestamp: Date.now() / 1000,
284
+ lt: 5,
285
+ ts: Date.now() / 1000,
286
286
  ops: [
287
287
  {
288
288
  key: 'cube-1',
289
- otype: 'color.set',
289
+ ot: 'color.set',
290
290
  path: 'color',
291
291
  value: '#00ff00',
292
292
  },
293
293
  {
294
294
  key: 'cube-1',
295
- otype: 'number.set',
295
+ ot: 'number.set',
296
296
  path: 'opacity',
297
297
  value: 0.5,
298
298
  },
299
299
  {
300
300
  key: 'cube-1',
301
- otype: 'vector3.add',
301
+ ot: 'vector3.add',
302
302
  path: 'transform.position',
303
303
  value: [0, 2, 0], // Move up by 2
304
304
  },
@@ -306,11 +306,11 @@ const msg5: CRDTMessage = {
306
306
  };
307
307
 
308
308
  console.log('Message 5 (Batch - Same Node):');
309
- console.log(' Envelope: lamport=%d', msg5.lamportTime);
309
+ console.log(' Envelope: lamport=%d', msg5.lt);
310
310
  console.log(' Operations on "%s": %d ops', msg5.ops[0].key, msg5.ops.length);
311
311
  msg5.ops.forEach((op) => {
312
312
  if ('value' in op) {
313
- console.log(' ✓ %s: %s = %s', op.otype, op.path, JSON.stringify(op.value));
313
+ console.log(' ✓ %s: %s = %s', op.ot, op.path, JSON.stringify(op.value));
314
314
  }
315
315
  });
316
316
 
@@ -321,22 +321,22 @@ console.log('\n🔀 Step 6: Compound Update - Multiple Nodes\n');
321
321
 
322
322
  const msg6: CRDTMessage = {
323
323
  id: 'msg-006',
324
- sessionId: 'session-bob',
324
+ client: 'session-bob',
325
325
  clock: { 'session-bob': 2 },
326
- lamportTime: 6,
327
- timestamp: Date.now() / 1000,
326
+ lt: 6,
327
+ ts: Date.now() / 1000,
328
328
  ops: [
329
329
  // Update cube
330
330
  {
331
331
  key: 'cube-1',
332
- otype: 'number.set',
332
+ ot: 'number.set',
333
333
  path: 'metalness',
334
334
  value: 0.8,
335
335
  },
336
336
  // Update sphere
337
337
  {
338
338
  key: 'sphere-1',
339
- otype: 'color.set',
339
+ ot: 'color.set',
340
340
  path: 'color',
341
341
  value: '#ffff00',
342
342
  },
@@ -344,11 +344,11 @@ const msg6: CRDTMessage = {
344
344
  };
345
345
 
346
346
  console.log('Message 6 (Compound - Different Nodes):');
347
- console.log(' Envelope: lamport=%d', msg6.lamportTime);
347
+ console.log(' Envelope: lamport=%d', msg6.lt);
348
348
  console.log(' Operations: %d nodes updated', new Set(msg6.ops.map((op) => op.key)).size);
349
349
  msg6.ops.forEach((op) => {
350
350
  if ('value' in op) {
351
- console.log(' ✓ %s: %s.%s = %s', op.otype, op.key, op.path, JSON.stringify(op.value));
351
+ console.log(' ✓ %s: %s.%s = %s', op.ot, op.key, op.path, JSON.stringify(op.value));
352
352
  }
353
353
  });
354
354
 
@@ -359,20 +359,20 @@ console.log('\n🎯 Step 7: Multi-Select Drag\n');
359
359
 
360
360
  const msg7: CRDTMessage = {
361
361
  id: 'msg-007',
362
- sessionId: 'session-alice',
362
+ client: 'session-alice',
363
363
  clock: { 'session-alice': 4 },
364
- lamportTime: 7,
365
- timestamp: Date.now() / 1000,
364
+ lt: 7,
365
+ ts: Date.now() / 1000,
366
366
  ops: [
367
367
  {
368
368
  key: 'cube-1',
369
- otype: 'vector3.add',
369
+ ot: 'vector3.add',
370
370
  path: 'transform.position',
371
371
  value: [3, 0, 0],
372
372
  },
373
373
  {
374
374
  key: 'sphere-1',
375
- otype: 'vector3.add',
375
+ ot: 'vector3.add',
376
376
  path: 'transform.position',
377
377
  value: [3, 0, 0],
378
378
  },
@@ -395,14 +395,14 @@ console.log('\n➕ Step 8: Additive Score (Concurrent)\n');
395
395
 
396
396
  const msg8a: CRDTMessage = {
397
397
  id: 'msg-008a',
398
- sessionId: 'session-alice',
398
+ client: 'session-alice',
399
399
  clock: { 'session-alice': 5 },
400
- lamportTime: 8,
401
- timestamp: Date.now() / 1000,
400
+ lt: 8,
401
+ ts: Date.now() / 1000,
402
402
  ops: [
403
403
  {
404
404
  key: 'cube-1',
405
- otype: 'number.add',
405
+ ot: 'number.add',
406
406
  path: 'score',
407
407
  value: 10,
408
408
  },
@@ -411,14 +411,14 @@ const msg8a: CRDTMessage = {
411
411
 
412
412
  const msg8b: CRDTMessage = {
413
413
  id: 'msg-008b',
414
- sessionId: 'session-bob',
414
+ client: 'session-bob',
415
415
  clock: { 'session-bob': 3 },
416
- lamportTime: 9,
417
- timestamp: Date.now() / 1000,
416
+ lt: 9,
417
+ ts: Date.now() / 1000,
418
418
  ops: [
419
419
  {
420
420
  key: 'cube-1',
421
- otype: 'number.add',
421
+ ot: 'number.add',
422
422
  path: 'score',
423
423
  value: 5,
424
424
  },
@@ -426,9 +426,9 @@ const msg8b: CRDTMessage = {
426
426
  };
427
427
 
428
428
  console.log('Message 8a (Alice):');
429
- console.log(' ✓ %s: cube-1.score += 10', msg8a.ops[0].otype);
429
+ console.log(' ✓ %s: cube-1.score += 10', msg8a.ops[0].ot);
430
430
  console.log('Message 8b (Bob - concurrent):');
431
- console.log(' ✓ %s: cube-1.score += 5', msg8b.ops[0].otype);
431
+ console.log(' ✓ %s: cube-1.score += 5', msg8b.ops[0].ot);
432
432
  console.log(' → Final score: 15 (both additions applied!)');
433
433
 
434
434
  // ========================================
@@ -438,15 +438,15 @@ console.log('\n🔄 Step 9: Reparent Node\n');
438
438
 
439
439
  const msg9: CRDTMessage = {
440
440
  id: 'msg-009',
441
- sessionId: 'session-alice',
441
+ client: 'session-alice',
442
442
  clock: { 'session-alice': 6 },
443
- lamportTime: 10,
444
- timestamp: Date.now() / 1000,
443
+ lt: 10,
444
+ ts: Date.now() / 1000,
445
445
  ops: [
446
446
  // Create new parent
447
447
  {
448
448
  key: 'group-1',
449
- otype: 'node.insert',
449
+ ot: 'node.insert',
450
450
  path: 'group-1',
451
451
  value: {
452
452
  key: 'uuid-group-001',
@@ -460,14 +460,14 @@ const msg9: CRDTMessage = {
460
460
  // Remove from old parent
461
461
  {
462
462
  key: 'scene',
463
- otype: 'array.remove',
463
+ ot: 'array.remove',
464
464
  path: 'children',
465
465
  value: 'cube-1',
466
466
  },
467
467
  // Add to new parent
468
468
  {
469
469
  key: 'group-1',
470
- otype: 'array.push',
470
+ ot: 'array.push',
471
471
  path: 'children',
472
472
  value: 'cube-1',
473
473
  },
@@ -477,11 +477,11 @@ const msg9: CRDTMessage = {
477
477
  console.log('Message 9 (Reparent - Compound):');
478
478
  console.log(' Operations: %d ops (atomic)', msg9.ops.length);
479
479
  msg9.ops.forEach((op, i) => {
480
- if (op.otype === 'node.insert') {
480
+ if (op.ot === 'node.insert') {
481
481
  console.log(' %d. Create group: %s', i + 1, op.key);
482
- } else if (op.otype === 'array.remove') {
482
+ } else if (op.ot === 'array.remove') {
483
483
  console.log(' %d. Remove from %s.children: "%s"', i + 1, op.key, op.value);
484
- } else if (op.otype === 'array.push') {
484
+ } else if (op.ot === 'array.push') {
485
485
  console.log(' %d. Add to %s.children: "%s"', i + 1, op.key, op.value);
486
486
  }
487
487
  });
@@ -494,22 +494,22 @@ console.log('\n🗑️ Step 10: Delete Node\n');
494
494
 
495
495
  const msg10: CRDTMessage = {
496
496
  id: 'msg-010',
497
- sessionId: 'session-bob',
497
+ client: 'session-bob',
498
498
  clock: { 'session-bob': 4 },
499
- lamportTime: 11,
500
- timestamp: Date.now() / 1000,
499
+ lt: 11,
500
+ ts: Date.now() / 1000,
501
501
  ops: [
502
502
  // Remove from parent
503
503
  {
504
504
  key: 'scene',
505
- otype: 'array.remove',
505
+ ot: 'array.remove',
506
506
  path: 'children',
507
507
  value: 'sphere-1',
508
508
  },
509
509
  // Delete node (tombstone)
510
510
  {
511
511
  key: 'sphere-1',
512
- otype: 'node.remove',
512
+ ot: 'node.remove',
513
513
  path: 'sphere-1',
514
514
  },
515
515
  ],
@@ -517,9 +517,9 @@ const msg10: CRDTMessage = {
517
517
 
518
518
  console.log('Message 10 (Delete):');
519
519
  msg10.ops.forEach((op, i) => {
520
- if (op.otype === 'array.remove') {
520
+ if (op.ot === 'array.remove') {
521
521
  console.log(' %d. Remove from parent: %s', i + 1, op.value);
522
- } else if (op.otype === 'node.remove') {
522
+ } else if (op.ot === 'node.remove') {
523
523
  console.log(' %d. Delete node: %s (tombstone)', i + 1, op.key);
524
524
  }
525
525
  });
@@ -549,7 +549,7 @@ console.log('\n' + '='.repeat(60));
549
549
  console.log('\n💡 Key Design Points:\n');
550
550
  console.log(' • CRDTMessage = envelope (metadata) + ops (batch)');
551
551
  console.log(' • Operations use `key` (human-friendly) not UUID');
552
- console.log(' • otype is explicit: "vector3.add" vs "vector3.set"');
552
+ console.log(' • ot is explicit: "vector3.add" vs "vector3.set"');
553
553
  console.log(' • True batching: one message, many nodes, atomic');
554
554
  console.log(' • No nested "properties" - clean operation structure');
555
555
  console.log('\n' + '='.repeat(60));
@@ -16,13 +16,13 @@ import type { GraphStore } from '@vuer-ai/vuer-rtc';
16
16
  /** Insert a scene root + a mesh node so property ops have a target. */
17
17
  function seedGraph(store: GraphStore): void {
18
18
  store.edit({
19
- otype: 'node.insert',
19
+ ot: 'node.insert',
20
20
  key: '',
21
21
  path: 'children',
22
22
  value: { key: 'scene', tag: 'Scene', name: 'Scene' },
23
23
  });
24
24
  store.edit({
25
- otype: 'node.insert',
25
+ ot: 'node.insert',
26
26
  key: 'scene',
27
27
  path: 'children',
28
28
  value: {
@@ -62,14 +62,14 @@ describe('convergence', () => {
62
62
  // Both clients edit the same node's position concurrently
63
63
  // (before seeing each other's edits)
64
64
  clientA.store.edit({
65
- otype: 'vector3.set',
65
+ ot: 'vector3.set',
66
66
  key: 'cube-1',
67
67
  path: 'position',
68
68
  value: [10, 0, 0],
69
69
  });
70
70
 
71
71
  clientB.store.edit({
72
- otype: 'vector3.set',
72
+ ot: 'vector3.set',
73
73
  key: 'cube-1',
74
74
  path: 'position',
75
75
  value: [0, 20, 0],
@@ -101,7 +101,7 @@ describe('convergence', () => {
101
101
 
102
102
  // Client A inserts a new node
103
103
  clientA.store.edit({
104
- otype: 'node.insert',
104
+ ot: 'node.insert',
105
105
  key: 'scene',
106
106
  path: 'children',
107
107
  value: {
@@ -114,7 +114,7 @@ describe('convergence', () => {
114
114
 
115
115
  // Client B edits a property on the existing node
116
116
  clientB.store.edit({
117
- otype: 'vector3.set',
117
+ ot: 'vector3.set',
118
118
  key: 'cube-1',
119
119
  path: 'position',
120
120
  value: [99, 99, 99],
@@ -149,7 +149,7 @@ describe('convergence', () => {
149
149
 
150
150
  // Client A edits and commits
151
151
  clientA.store.edit({
152
- otype: 'vector3.set',
152
+ ot: 'vector3.set',
153
153
  key: 'cube-1',
154
154
  path: 'position',
155
155
  value: [42, 42, 42],
@@ -187,7 +187,7 @@ describe('convergence', () => {
187
187
 
188
188
  // Client A sets position (vector3.set)
189
189
  clientA.store.edit({
190
- otype: 'vector3.set',
190
+ ot: 'vector3.set',
191
191
  key: 'cube-1',
192
192
  path: 'position',
193
193
  value: [1, 2, 3],
@@ -195,7 +195,7 @@ describe('convergence', () => {
195
195
 
196
196
  // Client B sets opacity (number.set) on the same node
197
197
  clientB.store.edit({
198
- otype: 'number.set',
198
+ ot: 'number.set',
199
199
  key: 'cube-1',
200
200
  path: 'opacity',
201
201
  value: 0.5,
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { GraphStore, SceneGraph, SceneNode } from '@vuer-ai/vuer-rtc';
9
+ import { getText } from '@vuer-ai/vuer-rtc';
9
10
 
10
11
  /**
11
12
  * Wait until `condition` returns true, polling every `interval` ms.
@@ -133,8 +134,29 @@ function nodeDataKeys(node: SceneNode): string[] {
133
134
  .sort();
134
135
  }
135
136
 
137
+ /** Check if a value is a TextRope instance */
138
+ function isTextRope(v: unknown): boolean {
139
+ return (
140
+ v != null &&
141
+ typeof v === 'object' &&
142
+ typeof (v as any).toString === 'function' &&
143
+ typeof (v as any).toJSON === 'function' &&
144
+ (v as any).agentId !== undefined &&
145
+ (v as any).clock !== undefined &&
146
+ (v as any)._tree !== undefined
147
+ );
148
+ }
149
+
136
150
  /** Simple recursive deep equality for JSON-safe values. */
137
151
  function deepEqual(a: unknown, b: unknown): boolean {
152
+ // Handle TextRope instances by converting to string
153
+ if (isTextRope(a)) {
154
+ a = getText(a as any);
155
+ }
156
+ if (isTextRope(b)) {
157
+ b = getText(b as any);
158
+ }
159
+
138
160
  if (a === b) return true;
139
161
  if (a == null || b == null) return a === b;
140
162
  if (typeof a !== typeof b) return false;
@@ -46,7 +46,7 @@ export interface TestServer {
46
46
  broker: InMemoryBroker;
47
47
  server: RTCServer;
48
48
  /** Connect a new GraphStore client to a room. */
49
- connectClient(roomId: string, sessionId: string, opts?: ConnectClientOptions): TestClient;
49
+ connectClient(roomId: string, client: string, opts?: ConnectClientOptions): TestClient;
50
50
  /** Tear down all connections and state. */
51
51
  close(): void;
52
52
  }
@@ -131,7 +131,7 @@ export function createTestServer(): TestServer {
131
131
 
132
132
  const clients: Array<{ cleanup: () => void }> = [];
133
133
 
134
- function connectClient(roomId: string, sessionId: string, opts?: ConnectClientOptions): TestClient {
134
+ function connectClient(roomId: string, client: string, opts?: ConnectClientOptions): TestClient {
135
135
  const { client: clientWs, server: serverWs } = createFakeWsPair();
136
136
  const latency = opts?.latencyMs ?? 0;
137
137
  const dropRate = opts?.dropRate ?? 0;
@@ -142,7 +142,7 @@ export function createTestServer(): TestServer {
142
142
 
143
143
  // Create a GraphStore that sends CRDTMessages over the fake WS (msgpack)
144
144
  const store = createGraph({
145
- sessionId,
145
+ client,
146
146
  onSend: (msg: CRDTMessage) => {
147
147
  clientWs.send(serialize({ mtype: 'crdt', msg }));
148
148
  },
@@ -170,7 +170,7 @@ export function createTestServer(): TestServer {
170
170
  });
171
171
 
172
172
  // Register the server-side WS with the RTCServer
173
- rtcServer.handleConnection(serverWs, roomId, sessionId);
173
+ rtcServer.handleConnection(serverWs, roomId, client);
174
174
 
175
175
  const retryUnacked = () => {
176
176
  const unacked = getUnackedMessages(store.getState());