@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.
- package/.env +1 -1
- package/README.md +56 -0
- package/dist/archive/ArchivalService.js +1 -1
- package/dist/archive/ArchivalService.js.map +1 -1
- package/dist/broker/InMemoryBroker.d.ts +2 -2
- package/dist/broker/InMemoryBroker.d.ts.map +1 -1
- package/dist/broker/InMemoryBroker.js +4 -4
- package/dist/broker/InMemoryBroker.js.map +1 -1
- package/dist/broker/types.d.ts +3 -3
- package/dist/broker/types.d.ts.map +1 -1
- package/dist/journal/CoalescingService.d.ts.map +1 -1
- package/dist/journal/CoalescingService.js +18 -208
- package/dist/journal/CoalescingService.js.map +1 -1
- package/dist/journal/GraphJournalService.d.ts +127 -0
- package/dist/journal/GraphJournalService.d.ts.map +1 -0
- package/dist/journal/GraphJournalService.js +491 -0
- package/dist/journal/GraphJournalService.js.map +1 -0
- package/dist/journal/JournalRLE.d.ts +2 -2
- package/dist/journal/JournalRLE.js +14 -14
- package/dist/journal/JournalRLE.js.map +1 -1
- package/dist/journal/JournalRepository.js +7 -7
- package/dist/journal/JournalRepository.js.map +1 -1
- package/dist/journal/JournalService.d.ts.map +1 -1
- package/dist/journal/JournalService.js +6 -40
- package/dist/journal/JournalService.js.map +1 -1
- package/dist/journal/RLECompression.d.ts +9 -9
- package/dist/journal/RLECompression.d.ts.map +1 -1
- package/dist/journal/RLECompression.js +22 -22
- package/dist/journal/RLECompression.js.map +1 -1
- package/dist/journal/TextJournalService.d.ts +98 -0
- package/dist/journal/TextJournalService.d.ts.map +1 -0
- package/dist/journal/TextJournalService.js +401 -0
- package/dist/journal/TextJournalService.js.map +1 -0
- package/dist/journal/index.d.ts +3 -1
- package/dist/journal/index.d.ts.map +1 -1
- package/dist/journal/index.js +4 -1
- package/dist/journal/index.js.map +1 -1
- package/dist/journal/rle-demo.js +11 -11
- package/dist/journal/rle-demo.js.map +1 -1
- package/dist/serve.d.ts +29 -11
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +558 -93
- package/dist/serve.js.map +1 -1
- package/dist/transport/RTCServer.d.ts +2 -2
- package/dist/transport/RTCServer.d.ts.map +1 -1
- package/dist/transport/RTCServer.js +22 -22
- package/dist/transport/RTCServer.js.map +1 -1
- package/docs/API.md +642 -0
- package/examples/compression-example.ts +3 -3
- package/package.json +2 -2
- package/prisma/schema.prisma +124 -6
- package/src/archive/ArchivalService.ts +1 -1
- package/src/broker/InMemoryBroker.ts +4 -4
- package/src/broker/types.ts +3 -3
- package/src/journal/CoalescingService.ts +18 -235
- package/src/journal/{JournalService.ts → GraphJournalService.ts} +34 -74
- package/src/journal/JournalRLE.ts +15 -15
- package/src/journal/JournalRepository.ts +7 -7
- package/src/journal/RLECompression.ts +24 -24
- package/src/journal/TextJournalService.ts +483 -0
- package/src/journal/index.ts +10 -2
- package/src/journal/rle-demo.ts +11 -11
- package/src/serve.ts +598 -94
- package/src/transport/RTCServer.ts +23 -23
- package/tests/benchmark/journal-optimization-benchmark.test.ts +14 -14
- package/tests/compression/compression.test.ts +8 -8
- package/tests/demo.ts +88 -88
- package/tests/e2e/convergence.test.ts +9 -9
- package/tests/e2e/helpers/assertions.ts +22 -0
- package/tests/e2e/helpers/createTestServer.ts +4 -4
- package/tests/e2e/latency.test.ts +47 -41
- package/tests/e2e/packet-loss.test.ts +6 -6
- package/tests/e2e/relay.test.ts +9 -9
- package/tests/e2e/sync-perf.test.ts +5 -5
- package/tests/e2e/sync-reconciliation.test.ts +6 -6
- package/tests/e2e/text-sync.test.ts +14 -14
- package/tests/e2e/tombstone-convergence.test.ts +22 -22
- package/tests/fixtures/array-ops.jsonl +6 -6
- package/tests/fixtures/boolean-ops.jsonl +6 -6
- package/tests/fixtures/color-ops.jsonl +4 -4
- package/tests/fixtures/edit-buffer.jsonl +3 -3
- package/tests/fixtures/messages.jsonl +4 -4
- package/tests/fixtures/node-ops.jsonl +6 -6
- package/tests/fixtures/number-ops.jsonl +7 -7
- package/tests/fixtures/object-ops.jsonl +4 -4
- package/tests/fixtures/operations.jsonl +7 -7
- package/tests/fixtures/string-ops.jsonl +4 -4
- package/tests/fixtures/undo-redo.jsonl +3 -3
- package/tests/fixtures/vector-ops.jsonl +9 -9
- package/tests/integration/repositories.test.ts +8 -9
- package/tests/journal/compaction-load-bug.test.ts +31 -31
- package/tests/journal/compaction.test.ts +26 -26
- package/tests/journal/journal-rle.test.ts +38 -38
- package/tests/journal/journal-service.test.ts +13 -13
- package/tests/journal/lww-ordering-bug.test.ts +39 -39
- package/tests/journal/rle-compression.test.ts +71 -71
- package/tests/journal/text-coalescing.test.ts +34 -34
- package/tests/test-data/datatypes.ts +85 -85
- package/tests/test-data/operations-example.ts +62 -62
- package/tests/test-data/scene-example.ts +11 -11
- package/tests/unit/operations.test.ts +7 -7
- package/tests/unit/s3-compression.test.ts +5 -3
- package/tests/unit/vectorClock.test.ts +2 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
+
ot: 'number.add',
|
|
203
203
|
key: 'cube-1',
|
|
204
204
|
path: 'opacity',
|
|
205
205
|
value: 5,
|
package/tests/e2e/relay.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
193
|
-
const textB = graphB.nodes['text-doc'].content
|
|
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');
|