@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
@@ -9,19 +9,25 @@ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
9
9
  import { createTestServer, type TestServer } from './helpers/createTestServer.js';
10
10
  import { waitForConvergence, assertGraphsEqual } from './helpers/assertions.js';
11
11
  import type { GraphStore } from '@vuer-ai/vuer-rtc';
12
+ import { getText } from '@vuer-ai/vuer-rtc';
13
+
14
+ /** Safe wrapper to get text content from node properties (which are typed as unknown) */
15
+ function getNodeText(content: unknown): string {
16
+ return getText(content as any);
17
+ }
12
18
 
13
19
  // ── Helpers ───────────────────────────────────────────────────
14
20
 
15
21
  /** Insert a scene root + a mesh node so property ops have a target. */
16
22
  function seedGraph(store: GraphStore): void {
17
23
  store.edit({
18
- otype: 'node.insert',
24
+ ot: 'node.insert',
19
25
  key: '',
20
26
  path: 'children',
21
27
  value: { key: 'scene', tag: 'Scene', name: 'Scene' },
22
28
  });
23
29
  store.edit({
24
- otype: 'node.insert',
30
+ ot: 'node.insert',
25
31
  key: 'scene',
26
32
  path: 'children',
27
33
  value: {
@@ -38,19 +44,19 @@ function seedGraph(store: GraphStore): void {
38
44
  /** Insert a scene root + a text doc node. */
39
45
  function seedTextGraph(store: GraphStore): void {
40
46
  store.edit({
41
- otype: 'node.insert',
47
+ ot: 'node.insert',
42
48
  key: '',
43
49
  path: 'children',
44
50
  value: { key: 'scene', tag: 'Scene', name: 'Scene' },
45
51
  });
46
52
  store.edit({
47
- otype: 'node.insert',
53
+ ot: 'node.insert',
48
54
  key: 'scene',
49
55
  path: 'children',
50
56
  value: { key: 'text-doc', tag: 'Text', name: 'Doc' },
51
57
  });
52
58
  store.edit({
53
- otype: 'text.init',
59
+ ot: 'text.init',
54
60
  key: 'text-doc',
55
61
  path: 'content',
56
62
  });
@@ -80,7 +86,7 @@ describe('latency', () => {
80
86
  await waitForConvergence([clientA.store, clientB.store], 5000);
81
87
 
82
88
  clientA.store.edit({
83
- otype: 'vector3.set',
89
+ ot: 'vector3.set',
84
90
  key: 'cube-1',
85
91
  path: 'position',
86
92
  value: [10, 20, 30],
@@ -106,13 +112,13 @@ describe('latency', () => {
106
112
 
107
113
  // Both edit concurrently
108
114
  clientA.store.edit({
109
- otype: 'vector3.set',
115
+ ot: 'vector3.set',
110
116
  key: 'cube-1',
111
117
  path: 'position',
112
118
  value: [1, 0, 0],
113
119
  });
114
120
  clientB.store.edit({
115
- otype: 'vector3.set',
121
+ ot: 'vector3.set',
116
122
  key: 'cube-1',
117
123
  path: 'position',
118
124
  value: [0, 1, 0],
@@ -139,13 +145,13 @@ describe('latency', () => {
139
145
 
140
146
  // A edits position, B edits opacity — no conflict
141
147
  clientA.store.edit({
142
- otype: 'vector3.set',
148
+ ot: 'vector3.set',
143
149
  key: 'cube-1',
144
150
  path: 'position',
145
151
  value: [5, 5, 5],
146
152
  });
147
153
  clientB.store.edit({
148
- otype: 'number.set',
154
+ ot: 'number.set',
149
155
  key: 'cube-1',
150
156
  path: 'opacity',
151
157
  value: 0.3,
@@ -176,7 +182,7 @@ describe('latency', () => {
176
182
  // Client A sends 10 rapid number.add ops
177
183
  for (let i = 0; i < 10; i++) {
178
184
  clientA.store.edit({
179
- otype: 'number.add',
185
+ ot: 'number.add',
180
186
  key: 'cube-1',
181
187
  path: 'opacity',
182
188
  value: 1,
@@ -206,19 +212,19 @@ describe('latency', () => {
206
212
 
207
213
  // All three edit concurrently
208
214
  clientA.store.edit({
209
- otype: 'vector3.set',
215
+ ot: 'vector3.set',
210
216
  key: 'cube-1',
211
217
  path: 'position',
212
218
  value: [1, 0, 0],
213
219
  });
214
220
  clientB.store.edit({
215
- otype: 'number.set',
221
+ ot: 'number.set',
216
222
  key: 'cube-1',
217
223
  path: 'opacity',
218
224
  value: 0.5,
219
225
  });
220
226
  clientC.store.edit({
221
- otype: 'node.insert',
227
+ ot: 'node.insert',
222
228
  key: 'scene',
223
229
  path: 'children',
224
230
  value: { key: 'sphere-1', tag: 'Mesh', name: 'Sphere' },
@@ -250,7 +256,7 @@ describe('latency', () => {
250
256
  await waitForConvergence([clientA.store, clientB.store], 5000);
251
257
 
252
258
  clientA.store.edit({
253
- otype: 'vector3.set',
259
+ ot: 'vector3.set',
254
260
  key: 'cube-1',
255
261
  path: 'position',
256
262
  value: [42, 42, 42],
@@ -284,26 +290,26 @@ describe('latency', () => {
284
290
 
285
291
  // Client A inserts "Hello"
286
292
  clientA.store.edit({
287
- otype: 'text.insert',
293
+ ot: 'text.insert',
288
294
  key: 'text-doc',
289
295
  path: 'content',
290
296
  position: 0,
291
- value: 'Hello',
297
+ value: [null, 'Hello'],
292
298
  });
293
299
  clientA.store.commit('A types Hello');
294
300
 
295
301
  await waitForConvergence([clientA.store, clientB.store], 5000);
296
302
 
297
- const contentB = clientB.store.getState().graph.nodes['text-doc'].content;
303
+ const contentB = getNodeText(clientB.store.getState().graph.nodes['text-doc'].content);
298
304
  expect(contentB).toBe('Hello');
299
305
 
300
306
  // Client B appends " World" (after seeing A's edit)
301
307
  clientB.store.edit({
302
- otype: 'text.insert',
308
+ ot: 'text.insert',
303
309
  key: 'text-doc',
304
310
  path: 'content',
305
311
  position: 5,
306
- value: ' World',
312
+ value: [null, ' World'],
307
313
  });
308
314
  clientB.store.commit('B types World');
309
315
 
@@ -312,7 +318,7 @@ describe('latency', () => {
312
318
  const graphA = clientA.store.getState().graph;
313
319
  const graphB = clientB.store.getState().graph;
314
320
  assertGraphsEqual(graphA, graphB);
315
- expect(graphA.nodes['text-doc'].content).toBe('Hello World');
321
+ expect(getNodeText(graphA.nodes['text-doc'].content)).toBe('Hello World');
316
322
  });
317
323
 
318
324
  // ── 8. High latency text edits (regression test) ────────────
@@ -330,11 +336,11 @@ describe('latency', () => {
330
336
  const chars = 'abcdef';
331
337
  for (let i = 0; i < chars.length; i++) {
332
338
  clientA.store.edit({
333
- otype: 'text.insert',
339
+ ot: 'text.insert',
334
340
  key: 'text-doc',
335
341
  path: 'content',
336
342
  position: i,
337
- value: chars[i],
343
+ value: [null, chars[i]],
338
344
  });
339
345
  clientA.store.commit(`type ${chars[i]}`);
340
346
  }
@@ -346,8 +352,8 @@ describe('latency', () => {
346
352
  assertGraphsEqual(graphA, graphB);
347
353
 
348
354
  // Both clients should have exactly "abcdef" — no duplication
349
- expect(graphA.nodes['text-doc'].content).toBe('abcdef');
350
- expect(graphB.nodes['text-doc'].content).toBe('abcdef');
355
+ expect(getNodeText(graphA.nodes['text-doc'].content)).toBe('abcdef');
356
+ expect(getNodeText(graphB.nodes['text-doc'].content)).toBe('abcdef');
351
357
  });
352
358
 
353
359
  // ── 9. Concurrent node inserts produce deterministic children order ──
@@ -360,11 +366,11 @@ describe('latency', () => {
360
366
  await waitForConvergence([clientA.store, clientB.store], 5000);
361
367
 
362
368
  const sceneKey = Object.values(clientA.store.getState().graph.nodes)
363
- .find((n) => n.tag === 'Scene' && !n._crdt?.deletedAt)!.key;
369
+ .find((n) => n.tag === 'Scene' && !(n._crdt as any)?.deletedAt)!.key;
364
370
 
365
371
  // Both clients insert a sphere concurrently (before seeing each other's ops)
366
372
  clientA.store.edit({
367
- otype: 'node.insert',
373
+ ot: 'node.insert',
368
374
  key: sceneKey,
369
375
  path: 'children',
370
376
  value: { key: 'sphere-a', tag: 'Mesh', name: 'Sphere A' },
@@ -372,7 +378,7 @@ describe('latency', () => {
372
378
  clientA.store.commit('insert sphere A');
373
379
 
374
380
  clientB.store.edit({
375
- otype: 'node.insert',
381
+ ot: 'node.insert',
376
382
  key: sceneKey,
377
383
  path: 'children',
378
384
  value: { key: 'sphere-b', tag: 'Mesh', name: 'Sphere B' },
@@ -404,9 +410,9 @@ describe('latency', () => {
404
410
 
405
411
  // Client A inserts a sphere before previous ops are acked
406
412
  clientA.store.edit({
407
- otype: 'node.insert',
413
+ ot: 'node.insert',
408
414
  key: Object.values(clientA.store.getState().graph.nodes)
409
- .find((n) => n.tag === 'Scene' && !n._crdt?.deletedAt)!.key,
415
+ .find((n) => n.tag === 'Scene' && !(n._crdt as any)?.deletedAt)!.key,
410
416
  path: 'children',
411
417
  value: { key: 'sphere-hi-lat', tag: 'Mesh', name: 'Sphere' },
412
418
  });
@@ -435,11 +441,11 @@ describe('latency', () => {
435
441
  const textA = 'does this work?';
436
442
  for (let i = 0; i < textA.length; i++) {
437
443
  clientA.store.edit({
438
- otype: 'text.insert',
444
+ ot: 'text.insert',
439
445
  key: 'text-doc',
440
446
  path: 'content',
441
447
  position: i,
442
- value: textA[i],
448
+ value: [null, textA[i]],
443
449
  });
444
450
  clientA.store.commit(`A types '${textA[i]}'`);
445
451
  }
@@ -448,11 +454,11 @@ describe('latency', () => {
448
454
  const textB = '\n1. \n2. \n3. ';
449
455
  for (let i = 0; i < textB.length; i++) {
450
456
  clientB.store.edit({
451
- otype: 'text.insert',
457
+ ot: 'text.insert',
452
458
  key: 'text-doc',
453
459
  path: 'content',
454
460
  position: i,
455
- value: textB[i],
461
+ value: [null, textB[i]],
456
462
  });
457
463
  clientB.store.commit(`B types '${textB[i]}'`);
458
464
  }
@@ -464,8 +470,8 @@ describe('latency', () => {
464
470
  assertGraphsEqual(graphA, graphB);
465
471
 
466
472
  // Both must see the same text content (exact order is CRDT-determined)
467
- expect(graphA.nodes['text-doc'].content).toBe(
468
- graphB.nodes['text-doc'].content,
473
+ expect(getNodeText(graphA.nodes['text-doc'].content)).toBe(
474
+ getNodeText(graphB.nodes['text-doc'].content),
469
475
  );
470
476
  });
471
477
 
@@ -480,21 +486,21 @@ describe('latency', () => {
480
486
 
481
487
  // A types "Hello" at position 0
482
488
  clientA.store.edit({
483
- otype: 'text.insert',
489
+ ot: 'text.insert',
484
490
  key: 'text-doc',
485
491
  path: 'content',
486
492
  position: 0,
487
- value: 'Hello',
493
+ value: [null, 'Hello'],
488
494
  });
489
495
  clientA.store.commit('A types Hello');
490
496
 
491
497
  // B types "World" at position 0 (concurrently, before seeing A's edit)
492
498
  clientB.store.edit({
493
- otype: 'text.insert',
499
+ ot: 'text.insert',
494
500
  key: 'text-doc',
495
501
  path: 'content',
496
502
  position: 0,
497
- value: 'World',
503
+ value: [null, 'World'],
498
504
  });
499
505
  clientB.store.commit('B types World');
500
506
 
@@ -505,7 +511,7 @@ describe('latency', () => {
505
511
  assertGraphsEqual(graphA, graphB);
506
512
 
507
513
  // Both should have both words (exact order is CRDT-determined)
508
- const content = graphA.nodes['text-doc'].content;
514
+ const content = getNodeText(graphA.nodes['text-doc'].content);
509
515
  expect(content).toContain('Hello');
510
516
  expect(content).toContain('World');
511
517
  });
@@ -14,13 +14,13 @@ import type { GraphStore } from '@vuer-ai/vuer-rtc';
14
14
 
15
15
  function seedGraph(store: GraphStore): void {
16
16
  store.edit({
17
- otype: 'node.insert',
17
+ ot: 'node.insert',
18
18
  key: '',
19
19
  path: 'children',
20
20
  value: { key: 'scene', tag: 'Scene', name: 'Scene' },
21
21
  });
22
22
  store.edit({
23
- otype: 'node.insert',
23
+ ot: 'node.insert',
24
24
  key: 'scene',
25
25
  path: 'children',
26
26
  value: {
@@ -90,7 +90,7 @@ describe('packet-loss', () => {
90
90
  // B has 50% drop — acks from server may be lost
91
91
 
92
92
  clientA.store.edit({
93
- otype: 'vector3.set',
93
+ ot: 'vector3.set',
94
94
  key: 'cube-1',
95
95
  path: 'position',
96
96
  value: [10, 20, 30],
@@ -98,7 +98,7 @@ describe('packet-loss', () => {
98
98
  clientA.store.commit('move cube');
99
99
 
100
100
  clientB.store.edit({
101
- otype: 'number.set',
101
+ ot: 'number.set',
102
102
  key: 'cube-1',
103
103
  path: 'opacity',
104
104
  value: 0.7,
@@ -156,7 +156,7 @@ describe('packet-loss', () => {
156
156
 
157
157
  // Make an edit and commit
158
158
  clientA.store.edit({
159
- otype: 'vector3.set',
159
+ ot: 'vector3.set',
160
160
  key: 'cube-1',
161
161
  path: 'position',
162
162
  value: [99, 99, 99],
@@ -199,7 +199,7 @@ describe('packet-loss', () => {
199
199
  // For deterministic behavior, we just retry until converged
200
200
 
201
201
  clientA.store.edit({
202
- otype: 'number.add',
202
+ ot: 'number.add',
203
203
  key: 'cube-1',
204
204
  path: 'opacity',
205
205
  value: 5,
@@ -16,13 +16,13 @@ import type { GraphStore, CRDTMessage } 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: {
@@ -63,7 +63,7 @@ describe('relay', () => {
63
63
 
64
64
  // Client A edits position
65
65
  clientA.store.edit({
66
- otype: 'vector3.set',
66
+ ot: 'vector3.set',
67
67
  key: 'cube-1',
68
68
  path: 'position',
69
69
  value: [1, 2, 3],
@@ -98,7 +98,7 @@ describe('relay', () => {
98
98
 
99
99
  // Now make an edit and commit
100
100
  clientA.store.edit({
101
- otype: 'vector3.set',
101
+ ot: 'vector3.set',
102
102
  key: 'cube-1',
103
103
  path: 'position',
104
104
  value: [5, 5, 5],
@@ -132,7 +132,7 @@ describe('relay', () => {
132
132
 
133
133
  // Client A edits and commits
134
134
  clientA.store.edit({
135
- otype: 'vector3.set',
135
+ ot: 'vector3.set',
136
136
  key: 'cube-1',
137
137
  path: 'position',
138
138
  value: [10, 20, 30],
@@ -184,7 +184,7 @@ describe('relay', () => {
184
184
 
185
185
  // Client A sends multiple commits
186
186
  clientA.store.edit({
187
- otype: 'vector3.set',
187
+ ot: 'vector3.set',
188
188
  key: 'cube-1',
189
189
  path: 'position',
190
190
  value: [1, 0, 0],
@@ -192,7 +192,7 @@ describe('relay', () => {
192
192
  clientA.store.commit('move 1');
193
193
 
194
194
  clientA.store.edit({
195
- otype: 'vector3.set',
195
+ ot: 'vector3.set',
196
196
  key: 'cube-1',
197
197
  path: 'position',
198
198
  value: [2, 0, 0],
@@ -200,7 +200,7 @@ describe('relay', () => {
200
200
  clientA.store.commit('move 2');
201
201
 
202
202
  clientA.store.edit({
203
- otype: 'vector3.set',
203
+ ot: 'vector3.set',
204
204
  key: 'cube-1',
205
205
  path: 'position',
206
206
  value: [3, 0, 0],
@@ -231,7 +231,7 @@ describe('relay', () => {
231
231
 
232
232
  // Client A edits position
233
233
  clientA.store.edit({
234
- otype: 'vector3.set',
234
+ ot: 'vector3.set',
235
235
  key: 'cube-1',
236
236
  path: 'position',
237
237
  value: [7, 8, 9],
@@ -14,13 +14,13 @@ import { waitFor, waitForConvergence, assertGraphsEqual } from './helpers/assert
14
14
 
15
15
  function seedGraph(store: GraphStore): void {
16
16
  store.edit({
17
- otype: 'node.insert',
17
+ ot: 'node.insert',
18
18
  key: '',
19
19
  path: 'children',
20
20
  value: { key: 'scene', tag: 'Scene', name: 'Scene' },
21
21
  });
22
22
  store.edit({
23
- otype: 'node.insert',
23
+ ot: 'node.insert',
24
24
  key: 'scene',
25
25
  path: 'children',
26
26
  value: {
@@ -38,7 +38,7 @@ function seedGraph(store: GraphStore): void {
38
38
  function generateEdits(store: GraphStore, n: number): void {
39
39
  for (let i = 0; i < n; i++) {
40
40
  store.edit({
41
- otype: 'vector3.set',
41
+ ot: 'vector3.set',
42
42
  key: 'cube-1',
43
43
  path: 'position',
44
44
  value: [i, i * 2, i * 3],
@@ -222,7 +222,7 @@ describe('sync-perf', () => {
222
222
  for (let i = 0; i < N_CLIENTS; i++) {
223
223
  for (let j = 0; j < EDITS_PER_CLIENT; j++) {
224
224
  clients[i].store.edit({
225
- otype: 'number.set',
225
+ ot: 'number.set',
226
226
  key: 'cube-1',
227
227
  path: `prop_${i}_${j}`,
228
228
  value: i * 1000 + j,
@@ -299,7 +299,7 @@ describe('sync-perf', () => {
299
299
  for (let i = 0; i < BATCH_SIZE; i++) {
300
300
  const idx = batch * BATCH_SIZE + i;
301
301
  clientA.store.edit({
302
- otype: 'number.set',
302
+ ot: 'number.set',
303
303
  key: 'cube-1',
304
304
  path: `val_${idx % 100}`,
305
305
  value: idx,
@@ -14,13 +14,13 @@ import type { GraphStore } from '@vuer-ai/vuer-rtc';
14
14
 
15
15
  function seedGraph(store: GraphStore): void {
16
16
  store.edit({
17
- otype: 'node.insert',
17
+ ot: 'node.insert',
18
18
  key: '',
19
19
  path: 'children',
20
20
  value: { key: 'scene', tag: 'Scene', name: 'Scene' },
21
21
  });
22
22
  store.edit({
23
- otype: 'node.insert',
23
+ ot: 'node.insert',
24
24
  key: 'scene',
25
25
  path: 'children',
26
26
  value: {
@@ -95,7 +95,7 @@ describe('sync-reconciliation', () => {
95
95
 
96
96
  // A makes property edits (commutative, safe for out-of-order replay)
97
97
  clientA.store.edit({
98
- otype: 'vector3.set',
98
+ ot: 'vector3.set',
99
99
  key: 'cube-1',
100
100
  path: 'position',
101
101
  value: [10, 20, 30],
@@ -103,7 +103,7 @@ describe('sync-reconciliation', () => {
103
103
  clientA.store.commit('move cube');
104
104
 
105
105
  clientA.store.edit({
106
- otype: 'number.set',
106
+ ot: 'number.set',
107
107
  key: 'cube-1',
108
108
  path: 'opacity',
109
109
  value: 0.5,
@@ -198,7 +198,7 @@ describe('sync-reconciliation', () => {
198
198
 
199
199
  // Both make edits (some may be dropped)
200
200
  clientA.store.edit({
201
- otype: 'vector3.set',
201
+ ot: 'vector3.set',
202
202
  key: 'cube-1',
203
203
  path: 'position',
204
204
  value: [99, 88, 77],
@@ -206,7 +206,7 @@ describe('sync-reconciliation', () => {
206
206
  clientA.store.commit('move');
207
207
 
208
208
  clientB.store.edit({
209
- otype: 'number.set',
209
+ ot: 'number.set',
210
210
  key: 'cube-1',
211
211
  path: 'opacity',
212
212
  value: 0.3,
@@ -16,13 +16,13 @@ import type { GraphStore } from '@vuer-ai/vuer-rtc';
16
16
  /** Insert a scene root + a text node so text ops have a target. */
17
17
  function seedTextGraph(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: { key: 'text-doc', tag: 'Text', name: 'Doc' },
@@ -58,7 +58,7 @@ describe('text-sync', () => {
58
58
 
59
59
  // Init text on the text-doc node
60
60
  clientA.store.edit({
61
- otype: 'text.init',
61
+ ot: 'text.init',
62
62
  key: 'text-doc',
63
63
  path: 'content',
64
64
  value: '',
@@ -68,7 +68,7 @@ describe('text-sync', () => {
68
68
 
69
69
  // Insert "Hello"
70
70
  clientA.store.edit({
71
- otype: 'text.insert',
71
+ ot: 'text.insert',
72
72
  key: 'text-doc',
73
73
  path: 'content',
74
74
  position: 0,
@@ -83,8 +83,8 @@ describe('text-sync', () => {
83
83
  const graphB = clientB.store.getState().graph;
84
84
 
85
85
  assertGraphsEqual(graphA, graphB);
86
- expect(graphA.nodes['text-doc'].content).toBe('Hello');
87
- expect(graphB.nodes['text-doc'].content).toBe('Hello');
86
+ expect(String(graphA.nodes['text-doc'].content)).toBe('Hello');
87
+ expect(String(graphB.nodes['text-doc'].content)).toBe('Hello');
88
88
  });
89
89
 
90
90
  // ── 2. Text edits recover via sync after packet loss ────────
@@ -102,7 +102,7 @@ describe('text-sync', () => {
102
102
 
103
103
  // Alice inits text and types while Bob has 100% drop rate
104
104
  clientA.store.edit({
105
- otype: 'text.init',
105
+ ot: 'text.init',
106
106
  key: 'text-doc',
107
107
  path: 'content',
108
108
  value: '',
@@ -111,7 +111,7 @@ describe('text-sync', () => {
111
111
  await tick();
112
112
 
113
113
  clientA.store.edit({
114
- otype: 'text.insert',
114
+ ot: 'text.insert',
115
115
  key: 'text-doc',
116
116
  path: 'content',
117
117
  position: 0,
@@ -135,7 +135,7 @@ describe('text-sync', () => {
135
135
  const graphB = clientB.store.getState().graph;
136
136
 
137
137
  assertGraphsEqual(graphA, graphB);
138
- expect(graphB.nodes['text-doc'].content).toBe('Hello');
138
+ expect(String(graphB.nodes['text-doc'].content)).toBe('Hello');
139
139
  });
140
140
 
141
141
  // ── 3. Concurrent text edits converge ─────────────────────
@@ -150,7 +150,7 @@ describe('text-sync', () => {
150
150
 
151
151
  // Both clients init text
152
152
  clientA.store.edit({
153
- otype: 'text.init',
153
+ ot: 'text.init',
154
154
  key: 'text-doc',
155
155
  path: 'content',
156
156
  value: '',
@@ -160,7 +160,7 @@ describe('text-sync', () => {
160
160
 
161
161
  // Both clients type concurrently (before seeing each other's edits)
162
162
  clientA.store.edit({
163
- otype: 'text.insert',
163
+ ot: 'text.insert',
164
164
  key: 'text-doc',
165
165
  path: 'content',
166
166
  position: 0,
@@ -168,7 +168,7 @@ describe('text-sync', () => {
168
168
  } as any);
169
169
 
170
170
  clientB.store.edit({
171
- otype: 'text.insert',
171
+ ot: 'text.insert',
172
172
  key: 'text-doc',
173
173
  path: 'content',
174
174
  position: 0,
@@ -189,8 +189,8 @@ describe('text-sync', () => {
189
189
  assertGraphsEqual(graphA, graphB);
190
190
 
191
191
  // Both "AAA" and "BBB" should be present in the final text
192
- const textA = graphA.nodes['text-doc'].content as string;
193
- const textB = graphB.nodes['text-doc'].content as string;
192
+ const textA = String(graphA.nodes['text-doc'].content);
193
+ const textB = String(graphB.nodes['text-doc'].content);
194
194
  expect(textA).toBe(textB);
195
195
  expect(textA).toContain('AAA');
196
196
  expect(textA).toContain('BBB');