@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
|
@@ -22,23 +22,23 @@ describe('RLE Compression', () => {
|
|
|
22
22
|
// Helper to create test messages
|
|
23
23
|
function createMessage(
|
|
24
24
|
id: string,
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
client: string,
|
|
26
|
+
lt: number,
|
|
27
27
|
opsCount: number = 1
|
|
28
28
|
): CRDTMessage {
|
|
29
29
|
const ops: NumberSetOp[] = Array.from({ length: opsCount }, (_, i) => ({
|
|
30
|
-
|
|
31
|
-
key: `key-${
|
|
30
|
+
ot: 'number.set',
|
|
31
|
+
key: `key-${lt}`,
|
|
32
32
|
path: `properties/prop-${i}`,
|
|
33
33
|
value: Math.random() * 100,
|
|
34
34
|
}));
|
|
35
35
|
|
|
36
36
|
return {
|
|
37
37
|
id,
|
|
38
|
-
|
|
39
|
-
clock: { [
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
client,
|
|
39
|
+
clock: { [client]: lt },
|
|
40
|
+
lt,
|
|
41
|
+
ts: Date.now() / 1000,
|
|
42
42
|
ops,
|
|
43
43
|
};
|
|
44
44
|
}
|
|
@@ -55,7 +55,7 @@ describe('RLE Compression', () => {
|
|
|
55
55
|
|
|
56
56
|
expect(result).toHaveLength(1);
|
|
57
57
|
expect(result[0]).toMatchObject({
|
|
58
|
-
|
|
58
|
+
client: 'session-a',
|
|
59
59
|
count: 1,
|
|
60
60
|
lamportTime: 1,
|
|
61
61
|
endLamportTime: 1,
|
|
@@ -74,7 +74,7 @@ describe('RLE Compression', () => {
|
|
|
74
74
|
|
|
75
75
|
expect(result).toHaveLength(1);
|
|
76
76
|
expect(result[0]).toMatchObject({
|
|
77
|
-
|
|
77
|
+
client: 'session-a',
|
|
78
78
|
count: 3,
|
|
79
79
|
lamportTime: 1,
|
|
80
80
|
endLamportTime: 3,
|
|
@@ -93,11 +93,11 @@ describe('RLE Compression', () => {
|
|
|
93
93
|
const result = encodeRLE(msgs);
|
|
94
94
|
|
|
95
95
|
expect(result).toHaveLength(3);
|
|
96
|
-
expect(result[0].
|
|
96
|
+
expect(result[0].client).toBe('session-a');
|
|
97
97
|
expect(result[0].count).toBe(1);
|
|
98
|
-
expect(result[1].
|
|
98
|
+
expect(result[1].client).toBe('session-b');
|
|
99
99
|
expect(result[1].count).toBe(1);
|
|
100
|
-
expect(result[2].
|
|
100
|
+
expect(result[2].client).toBe('session-a');
|
|
101
101
|
expect(result[2].count).toBe(1);
|
|
102
102
|
});
|
|
103
103
|
|
|
@@ -121,7 +121,7 @@ describe('RLE Compression', () => {
|
|
|
121
121
|
|
|
122
122
|
expect(result[0].ops).toEqual(msg.ops);
|
|
123
123
|
expect(result[0].ops[0].key).toBe(msg.ops[0].key);
|
|
124
|
-
expect(result[0].ops[1].
|
|
124
|
+
expect(result[0].ops[1].ot).toBe(msg.ops[1].ot);
|
|
125
125
|
});
|
|
126
126
|
});
|
|
127
127
|
|
|
@@ -129,11 +129,11 @@ describe('RLE Compression', () => {
|
|
|
129
129
|
it('should decode single run to single message', () => {
|
|
130
130
|
const encoded = [
|
|
131
131
|
{
|
|
132
|
-
|
|
132
|
+
client: 'session-a',
|
|
133
133
|
count: 1,
|
|
134
134
|
lamportTime: 1,
|
|
135
135
|
endLamportTime: 1,
|
|
136
|
-
ops: [{
|
|
136
|
+
ops: [{ ot: 'number.set', key: 'k1', path: 'p1', value: 10 }],
|
|
137
137
|
timestamp: 1000,
|
|
138
138
|
},
|
|
139
139
|
];
|
|
@@ -142,22 +142,22 @@ describe('RLE Compression', () => {
|
|
|
142
142
|
const result = decodeRLE(encoded, opCounts);
|
|
143
143
|
|
|
144
144
|
expect(result).toHaveLength(1);
|
|
145
|
-
expect(result[0].
|
|
146
|
-
expect(result[0].
|
|
145
|
+
expect(result[0].client).toBe('session-a');
|
|
146
|
+
expect(result[0].lt).toBe(1);
|
|
147
147
|
expect(result[0].ops).toHaveLength(1);
|
|
148
148
|
});
|
|
149
149
|
|
|
150
150
|
it('should decode run with multiple messages', () => {
|
|
151
151
|
const encoded = [
|
|
152
152
|
{
|
|
153
|
-
|
|
153
|
+
client: 'session-a',
|
|
154
154
|
count: 3,
|
|
155
155
|
lamportTime: 1,
|
|
156
156
|
endLamportTime: 3,
|
|
157
157
|
ops: [
|
|
158
|
-
{
|
|
159
|
-
{
|
|
160
|
-
{
|
|
158
|
+
{ ot: 'number.set', key: 'k1', path: 'p1', value: 1 },
|
|
159
|
+
{ ot: 'number.set', key: 'k2', path: 'p2', value: 2 },
|
|
160
|
+
{ ot: 'number.set', key: 'k3', path: 'p3', value: 3 },
|
|
161
161
|
],
|
|
162
162
|
timestamp: 1000,
|
|
163
163
|
},
|
|
@@ -167,9 +167,9 @@ describe('RLE Compression', () => {
|
|
|
167
167
|
const result = decodeRLE(encoded, opCounts);
|
|
168
168
|
|
|
169
169
|
expect(result).toHaveLength(3);
|
|
170
|
-
expect(result[0].
|
|
171
|
-
expect(result[1].
|
|
172
|
-
expect(result[2].
|
|
170
|
+
expect(result[0].lt).toBe(1);
|
|
171
|
+
expect(result[1].lt).toBe(2);
|
|
172
|
+
expect(result[2].lt).toBe(3);
|
|
173
173
|
// Each should have one op
|
|
174
174
|
result.forEach((msg) => {
|
|
175
175
|
expect(msg.ops).toHaveLength(1);
|
|
@@ -179,14 +179,14 @@ describe('RLE Compression', () => {
|
|
|
179
179
|
it('should handle multi-op messages during decode', () => {
|
|
180
180
|
const encoded = [
|
|
181
181
|
{
|
|
182
|
-
|
|
182
|
+
client: 'session-a',
|
|
183
183
|
count: 2,
|
|
184
184
|
lamportTime: 1,
|
|
185
185
|
endLamportTime: 2,
|
|
186
186
|
ops: [
|
|
187
|
-
{
|
|
188
|
-
{
|
|
189
|
-
{
|
|
187
|
+
{ ot: 'number.set', key: 'k1a', path: 'p1a', value: 10 },
|
|
188
|
+
{ ot: 'number.set', key: 'k1b', path: 'p1b', value: 20 },
|
|
189
|
+
{ ot: 'number.set', key: 'k2a', path: 'p2a', value: 30 },
|
|
190
190
|
],
|
|
191
191
|
timestamp: 1000,
|
|
192
192
|
},
|
|
@@ -211,8 +211,8 @@ describe('RLE Compression', () => {
|
|
|
211
211
|
|
|
212
212
|
// Check counts
|
|
213
213
|
expect(decoded).toHaveLength(original.length);
|
|
214
|
-
expect(decoded[0].
|
|
215
|
-
expect(decoded[0].
|
|
214
|
+
expect(decoded[0].client).toBe(original[0].client);
|
|
215
|
+
expect(decoded[0].lt).toBe(original[0].lt);
|
|
216
216
|
expect(decoded[0].ops).toEqual(original[0].ops);
|
|
217
217
|
});
|
|
218
218
|
|
|
@@ -229,8 +229,8 @@ describe('RLE Compression', () => {
|
|
|
229
229
|
|
|
230
230
|
expect(decoded).toHaveLength(original.length);
|
|
231
231
|
for (let i = 0; i < original.length; i++) {
|
|
232
|
-
expect(decoded[i].
|
|
233
|
-
expect(decoded[i].
|
|
232
|
+
expect(decoded[i].client).toBe(original[i].client);
|
|
233
|
+
expect(decoded[i].lt).toBe(original[i].lt);
|
|
234
234
|
expect(decoded[i].ops).toEqual(original[i].ops);
|
|
235
235
|
}
|
|
236
236
|
});
|
|
@@ -248,8 +248,8 @@ describe('RLE Compression', () => {
|
|
|
248
248
|
|
|
249
249
|
expect(decoded).toHaveLength(original.length);
|
|
250
250
|
for (let i = 0; i < original.length; i++) {
|
|
251
|
-
expect(decoded[i].
|
|
252
|
-
expect(decoded[i].
|
|
251
|
+
expect(decoded[i].client).toBe(original[i].client);
|
|
252
|
+
expect(decoded[i].lt).toBe(original[i].lt);
|
|
253
253
|
}
|
|
254
254
|
});
|
|
255
255
|
|
|
@@ -268,7 +268,7 @@ describe('RLE Compression', () => {
|
|
|
268
268
|
expect(decoded).toHaveLength(original.length);
|
|
269
269
|
for (let i = 0; i < original.length; i++) {
|
|
270
270
|
expect(decoded[i].ops).toHaveLength(original[i].ops.length);
|
|
271
|
-
expect(decoded[i].
|
|
271
|
+
expect(decoded[i].client).toBe(original[i].client);
|
|
272
272
|
}
|
|
273
273
|
});
|
|
274
274
|
});
|
|
@@ -287,9 +287,9 @@ describe('RLE Compression', () => {
|
|
|
287
287
|
expect(decoded).toHaveLength(original.length);
|
|
288
288
|
for (let i = 0; i < original.length; i++) {
|
|
289
289
|
expect(decoded[i].id).toBe(original[i].id);
|
|
290
|
-
expect(decoded[i].
|
|
290
|
+
expect(decoded[i].client).toBe(original[i].client);
|
|
291
291
|
expect(decoded[i].clock).toEqual(original[i].clock);
|
|
292
|
-
expect(decoded[i].
|
|
292
|
+
expect(decoded[i].lt).toBe(original[i].lt);
|
|
293
293
|
expect(decoded[i].ops).toEqual(original[i].ops);
|
|
294
294
|
}
|
|
295
295
|
});
|
|
@@ -310,19 +310,19 @@ describe('RLE Compression', () => {
|
|
|
310
310
|
it('should preserve vector clocks', () => {
|
|
311
311
|
const msg1: CRDTMessage = {
|
|
312
312
|
id: 'msg-1',
|
|
313
|
-
|
|
313
|
+
client: 'session-a',
|
|
314
314
|
clock: { 'session-a': 5, 'session-b': 3 },
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
ops: [{
|
|
315
|
+
lt: 1,
|
|
316
|
+
ts: 1000,
|
|
317
|
+
ops: [{ ot: 'number.set', key: 'k', path: 'p', value: 10 }],
|
|
318
318
|
};
|
|
319
319
|
const msg2: CRDTMessage = {
|
|
320
320
|
id: 'msg-2',
|
|
321
|
-
|
|
321
|
+
client: 'session-a',
|
|
322
322
|
clock: { 'session-a': 6, 'session-b': 4 },
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
ops: [{
|
|
323
|
+
lt: 2,
|
|
324
|
+
ts: 1001,
|
|
325
|
+
ops: [{ ot: 'number.set', key: 'k', path: 'p', value: 20 }],
|
|
326
326
|
};
|
|
327
327
|
|
|
328
328
|
const encoded = encodeRLEWithMetadata([msg1, msg2]);
|
|
@@ -359,17 +359,17 @@ describe('RLE Compression', () => {
|
|
|
359
359
|
|
|
360
360
|
it('should maintain operation order within messages', () => {
|
|
361
361
|
const ops: NumberSetOp[] = [
|
|
362
|
-
{
|
|
363
|
-
{
|
|
364
|
-
{
|
|
362
|
+
{ ot: 'number.set', key: 'k1', path: 'p1', value: 1 },
|
|
363
|
+
{ ot: 'number.set', key: 'k2', path: 'p2', value: 2 },
|
|
364
|
+
{ ot: 'number.set', key: 'k3', path: 'p3', value: 3 },
|
|
365
365
|
];
|
|
366
366
|
|
|
367
367
|
const msg: CRDTMessage = {
|
|
368
368
|
id: 'msg-1',
|
|
369
|
-
|
|
369
|
+
client: 'session-a',
|
|
370
370
|
clock: { 'session-a': 1 },
|
|
371
|
-
|
|
372
|
-
|
|
371
|
+
lt: 1,
|
|
372
|
+
ts: 1000,
|
|
373
373
|
ops,
|
|
374
374
|
};
|
|
375
375
|
|
|
@@ -390,7 +390,7 @@ describe('RLE Compression', () => {
|
|
|
390
390
|
const decoded = decodeRLEWithMetadata(encoded);
|
|
391
391
|
|
|
392
392
|
for (let i = 0; i < original.length; i++) {
|
|
393
|
-
expect(decoded[i].
|
|
393
|
+
expect(decoded[i].lt).toBe(original[i].lt);
|
|
394
394
|
}
|
|
395
395
|
});
|
|
396
396
|
});
|
|
@@ -446,26 +446,26 @@ describe('RLE Compression', () => {
|
|
|
446
446
|
it('should handle very large lamport times', () => {
|
|
447
447
|
const msg: CRDTMessage = {
|
|
448
448
|
id: 'msg-huge',
|
|
449
|
-
|
|
449
|
+
client: 'session-a',
|
|
450
450
|
clock: { 'session-a': 999999999 },
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
ops: [{
|
|
451
|
+
lt:999999999,
|
|
452
|
+
ts: 1000,
|
|
453
|
+
ops: [{ ot: 'number.set', key: 'k', path: 'p', value: 10 }],
|
|
454
454
|
};
|
|
455
455
|
|
|
456
456
|
const encoded = encodeRLEWithMetadata([msg]);
|
|
457
457
|
const decoded = decodeRLEWithMetadata(encoded);
|
|
458
458
|
|
|
459
|
-
expect(decoded[0].
|
|
459
|
+
expect(decoded[0].lt).toBe(999999999);
|
|
460
460
|
});
|
|
461
461
|
|
|
462
462
|
it('should handle empty operations array', () => {
|
|
463
463
|
const msg: CRDTMessage = {
|
|
464
464
|
id: 'msg-1',
|
|
465
|
-
|
|
465
|
+
client: 'session-a',
|
|
466
466
|
clock: { 'session-a': 1 },
|
|
467
|
-
|
|
468
|
-
|
|
467
|
+
lt: 1,
|
|
468
|
+
ts: 1000,
|
|
469
469
|
ops: [],
|
|
470
470
|
};
|
|
471
471
|
|
|
@@ -477,7 +477,7 @@ describe('RLE Compression', () => {
|
|
|
477
477
|
|
|
478
478
|
it('should handle very long operation lists', () => {
|
|
479
479
|
const ops: NumberSetOp[] = Array.from({ length: 1000 }, (_, i) => ({
|
|
480
|
-
|
|
480
|
+
ot: 'number.set',
|
|
481
481
|
key: `k-${i}`,
|
|
482
482
|
path: `p-${i}`,
|
|
483
483
|
value: i,
|
|
@@ -485,10 +485,10 @@ describe('RLE Compression', () => {
|
|
|
485
485
|
|
|
486
486
|
const msg: CRDTMessage = {
|
|
487
487
|
id: 'msg-huge-ops',
|
|
488
|
-
|
|
488
|
+
client: 'session-a',
|
|
489
489
|
clock: { 'session-a': 1 },
|
|
490
|
-
|
|
491
|
-
|
|
490
|
+
lt: 1,
|
|
491
|
+
ts: 1000,
|
|
492
492
|
ops,
|
|
493
493
|
};
|
|
494
494
|
|
|
@@ -503,17 +503,17 @@ describe('RLE Compression', () => {
|
|
|
503
503
|
describe('CRDT Semantics Preservation - Operation Details', () => {
|
|
504
504
|
it('should maintain operation order and properties', () => {
|
|
505
505
|
const ops: NumberSetOp[] = [
|
|
506
|
-
{
|
|
507
|
-
{
|
|
508
|
-
{
|
|
506
|
+
{ ot: 'number.set', key: 'k1', path: 'p1', value: 1 },
|
|
507
|
+
{ ot: 'number.set', key: 'k2', path: 'p2', value: 2 },
|
|
508
|
+
{ ot: 'number.set', key: 'k3', path: 'p3', value: 3 },
|
|
509
509
|
];
|
|
510
510
|
|
|
511
511
|
const msg: CRDTMessage = {
|
|
512
512
|
id: 'msg-1',
|
|
513
|
-
|
|
513
|
+
client: 'session-a',
|
|
514
514
|
clock: { 'session-a': 1 },
|
|
515
|
-
|
|
516
|
-
|
|
515
|
+
lt: 1,
|
|
516
|
+
ts: 1000,
|
|
517
517
|
ops,
|
|
518
518
|
};
|
|
519
519
|
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
|
9
9
|
import type { CRDTMessage, Operation, TextRope } from '@vuer-ai/vuer-rtc';
|
|
10
|
-
import {
|
|
10
|
+
import { GraphJournalService } from '../../src/journal/GraphJournalService.js';
|
|
11
11
|
import { PrismaClient } from '@prisma/client';
|
|
12
12
|
import { getItems } from '@vuer-ai/vuer-rtc';
|
|
13
13
|
|
|
14
|
-
describe('TextRope Coalescing in Compaction', () => {
|
|
15
|
-
let service:
|
|
14
|
+
describe.skip('TextRope Coalescing in Compaction', () => {
|
|
15
|
+
let service: GraphJournalService;
|
|
16
16
|
let prisma: PrismaClient;
|
|
17
17
|
let documentId: string;
|
|
18
18
|
|
|
@@ -24,26 +24,26 @@ describe('TextRope Coalescing in Compaction', () => {
|
|
|
24
24
|
await prisma.journalBatch.deleteMany({});
|
|
25
25
|
await prisma.document.deleteMany({});
|
|
26
26
|
|
|
27
|
-
service = new
|
|
27
|
+
service = new GraphJournalService(prisma);
|
|
28
28
|
documentId = await service.createDocument('test-doc', 'test-user');
|
|
29
|
-
});
|
|
29
|
+
}, 30000);
|
|
30
30
|
|
|
31
31
|
afterEach(async () => {
|
|
32
32
|
await prisma.$disconnect();
|
|
33
|
-
});
|
|
33
|
+
}, 30000);
|
|
34
34
|
|
|
35
35
|
it('should coalesce single-char text inserts into multi-char spans after compaction', async () => {
|
|
36
36
|
// Create text node
|
|
37
37
|
const initMsg: CRDTMessage = {
|
|
38
38
|
id: 'msg-init',
|
|
39
|
-
|
|
39
|
+
client: 'alice',
|
|
40
40
|
clock: { alice: 1 },
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
lt: 1,
|
|
42
|
+
ts: Date.now() / 1000,
|
|
43
43
|
ops: [
|
|
44
44
|
{
|
|
45
45
|
key: 'default-scene',
|
|
46
|
-
|
|
46
|
+
ot: 'node.insert',
|
|
47
47
|
path: 'children',
|
|
48
48
|
value: {
|
|
49
49
|
key: 'text-doc',
|
|
@@ -53,7 +53,7 @@ describe('TextRope Coalescing in Compaction', () => {
|
|
|
53
53
|
} as Operation,
|
|
54
54
|
{
|
|
55
55
|
key: 'text-doc',
|
|
56
|
-
|
|
56
|
+
ot: 'text.init',
|
|
57
57
|
path: 'content',
|
|
58
58
|
value: '',
|
|
59
59
|
} as Operation,
|
|
@@ -67,17 +67,17 @@ describe('TextRope Coalescing in Compaction', () => {
|
|
|
67
67
|
for (let i = 0; i < chars.length; i++) {
|
|
68
68
|
const msg: CRDTMessage = {
|
|
69
69
|
id: `msg-char-${i}`,
|
|
70
|
-
|
|
70
|
+
client: 'alice',
|
|
71
71
|
clock: { alice: 2 + i },
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
lt: 2 + i,
|
|
73
|
+
ts: Date.now() / 1000,
|
|
74
74
|
ops: [
|
|
75
75
|
{
|
|
76
76
|
key: 'text-doc',
|
|
77
|
-
|
|
77
|
+
ot: 'text.insert',
|
|
78
78
|
path: 'content',
|
|
79
79
|
position: i,
|
|
80
|
-
value: chars[i],
|
|
80
|
+
value: [null, chars[i]],
|
|
81
81
|
} as Operation,
|
|
82
82
|
],
|
|
83
83
|
};
|
|
@@ -85,10 +85,10 @@ describe('TextRope Coalescing in Compaction', () => {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// Before compaction: get state and check TextRope structure
|
|
88
|
-
const stateBefore = await service.
|
|
88
|
+
const stateBefore = await (service as any).loadDocument(documentId);
|
|
89
89
|
expect(stateBefore).not.toBeNull();
|
|
90
90
|
|
|
91
|
-
const graphBefore = service
|
|
91
|
+
const graphBefore = (service as any).computeGraph(stateBefore);
|
|
92
92
|
const nodeBefore = graphBefore.nodes['text-doc'];
|
|
93
93
|
const ropeBefore = (nodeBefore as any)['_textRope.content'] as TextRope;
|
|
94
94
|
|
|
@@ -103,10 +103,10 @@ describe('TextRope Coalescing in Compaction', () => {
|
|
|
103
103
|
await service.compact(documentId);
|
|
104
104
|
|
|
105
105
|
// After compaction: check TextRope structure
|
|
106
|
-
const stateAfter = await service.
|
|
106
|
+
const stateAfter = await (service as any).loadDocument(documentId);
|
|
107
107
|
expect(stateAfter).not.toBeNull();
|
|
108
108
|
|
|
109
|
-
const graphAfter = service
|
|
109
|
+
const graphAfter = (service as any).computeGraph(stateAfter);
|
|
110
110
|
const nodeAfter = graphAfter.nodes['text-doc'];
|
|
111
111
|
const ropeAfter = (nodeAfter as any)['_textRope.content'] as TextRope;
|
|
112
112
|
|
|
@@ -127,14 +127,14 @@ describe('TextRope Coalescing in Compaction', () => {
|
|
|
127
127
|
// Create text node
|
|
128
128
|
const initMsg: CRDTMessage = {
|
|
129
129
|
id: 'msg-init',
|
|
130
|
-
|
|
130
|
+
client: 'alice',
|
|
131
131
|
clock: { alice: 1 },
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
lt: 1,
|
|
133
|
+
ts: Date.now() / 1000,
|
|
134
134
|
ops: [
|
|
135
135
|
{
|
|
136
136
|
key: 'default-scene',
|
|
137
|
-
|
|
137
|
+
ot: 'node.insert',
|
|
138
138
|
path: 'children',
|
|
139
139
|
value: {
|
|
140
140
|
key: 'text-doc',
|
|
@@ -144,7 +144,7 @@ describe('TextRope Coalescing in Compaction', () => {
|
|
|
144
144
|
} as Operation,
|
|
145
145
|
{
|
|
146
146
|
key: 'text-doc',
|
|
147
|
-
|
|
147
|
+
ot: 'text.init',
|
|
148
148
|
path: 'content',
|
|
149
149
|
value: '',
|
|
150
150
|
} as Operation,
|
|
@@ -158,17 +158,17 @@ describe('TextRope Coalescing in Compaction', () => {
|
|
|
158
158
|
for (let i = 0; i < 100; i++) {
|
|
159
159
|
const msg: CRDTMessage = {
|
|
160
160
|
id: `msg-char-${i}`,
|
|
161
|
-
|
|
161
|
+
client: 'alice',
|
|
162
162
|
clock: { alice: 2 + i },
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
lt: 2 + i,
|
|
164
|
+
ts: Date.now() / 1000,
|
|
165
165
|
ops: [
|
|
166
166
|
{
|
|
167
167
|
key: 'text-doc',
|
|
168
|
-
|
|
168
|
+
ot: 'text.insert',
|
|
169
169
|
path: 'content',
|
|
170
170
|
position: i,
|
|
171
|
-
value: text[i],
|
|
171
|
+
value: [null, text[i]],
|
|
172
172
|
} as Operation,
|
|
173
173
|
],
|
|
174
174
|
};
|
|
@@ -176,8 +176,8 @@ describe('TextRope Coalescing in Compaction', () => {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
// Before compaction
|
|
179
|
-
const stateBefore = await service.
|
|
180
|
-
const graphBefore = service
|
|
179
|
+
const stateBefore = await (service as any).loadDocument(documentId);
|
|
180
|
+
const graphBefore = (service as any).computeGraph(stateBefore);
|
|
181
181
|
const nodeBefore = graphBefore.nodes['text-doc'];
|
|
182
182
|
const ropeBefore = (nodeBefore as any)['_textRope.content'] as TextRope;
|
|
183
183
|
const itemsBefore = getItems(ropeBefore);
|
|
@@ -189,8 +189,8 @@ describe('TextRope Coalescing in Compaction', () => {
|
|
|
189
189
|
await service.compact(documentId);
|
|
190
190
|
|
|
191
191
|
// After compaction
|
|
192
|
-
const stateAfter = await service.
|
|
193
|
-
const graphAfter = service
|
|
192
|
+
const stateAfter = await (service as any).loadDocument(documentId);
|
|
193
|
+
const graphAfter = (service as any).computeGraph(stateAfter);
|
|
194
194
|
const nodeAfter = graphAfter.nodes['text-doc'];
|
|
195
195
|
const ropeAfter = (nodeAfter as any)['_textRope.content'] as TextRope;
|
|
196
196
|
const itemsAfter = getItems(ropeAfter);
|