@vuer-ai/vuer-rtc-server 0.2.0 → 0.2.2
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 -0
- package/S3_COMPRESSION_GUIDE.md +233 -0
- package/dist/archive/ArchivalService.d.ts +117 -0
- package/dist/archive/ArchivalService.d.ts.map +1 -0
- package/dist/archive/ArchivalService.js +181 -0
- package/dist/archive/ArchivalService.js.map +1 -0
- package/dist/broker/InMemoryBroker.d.ts +2 -0
- package/dist/broker/InMemoryBroker.d.ts.map +1 -1
- package/dist/broker/InMemoryBroker.js +4 -0
- package/dist/broker/InMemoryBroker.js.map +1 -1
- package/dist/compression/CompressionUtils.d.ts +57 -0
- package/dist/compression/CompressionUtils.d.ts.map +1 -0
- package/dist/compression/CompressionUtils.js +90 -0
- package/dist/compression/CompressionUtils.js.map +1 -0
- package/dist/compression/index.d.ts +7 -0
- package/dist/compression/index.d.ts.map +1 -0
- package/dist/compression/index.js +7 -0
- package/dist/compression/index.js.map +1 -0
- package/dist/journal/CoalescingService.d.ts +63 -0
- package/dist/journal/CoalescingService.d.ts.map +1 -0
- package/dist/journal/CoalescingService.js +507 -0
- package/dist/journal/CoalescingService.js.map +1 -0
- package/dist/journal/JournalRLE.d.ts +81 -0
- package/dist/journal/JournalRLE.d.ts.map +1 -0
- package/dist/journal/JournalRLE.js +199 -0
- package/dist/journal/JournalRLE.js.map +1 -0
- package/dist/journal/JournalService.d.ts +7 -3
- package/dist/journal/JournalService.d.ts.map +1 -1
- package/dist/journal/JournalService.js +152 -12
- package/dist/journal/JournalService.js.map +1 -1
- package/dist/journal/RLECompression.d.ts +73 -0
- package/dist/journal/RLECompression.d.ts.map +1 -0
- package/dist/journal/RLECompression.js +152 -0
- package/dist/journal/RLECompression.js.map +1 -0
- package/dist/journal/rle-demo.d.ts +8 -0
- package/dist/journal/rle-demo.d.ts.map +1 -0
- package/dist/journal/rle-demo.js +159 -0
- package/dist/journal/rle-demo.js.map +1 -0
- package/dist/persistence/S3ColdStorage.d.ts +62 -0
- package/dist/persistence/S3ColdStorage.d.ts.map +1 -0
- package/dist/persistence/S3ColdStorage.js +88 -0
- package/dist/persistence/S3ColdStorage.js.map +1 -0
- package/dist/persistence/S3ColdStorageIntegration.d.ts +78 -0
- package/dist/persistence/S3ColdStorageIntegration.d.ts.map +1 -0
- package/dist/persistence/S3ColdStorageIntegration.js +93 -0
- package/dist/persistence/S3ColdStorageIntegration.js.map +1 -0
- package/dist/serve.d.ts +2 -0
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +623 -15
- package/dist/serve.js.map +1 -1
- package/docs/RLE_COMPRESSION.md +397 -0
- package/examples/compression-example.ts +259 -0
- package/package.json +14 -14
- package/src/archive/ArchivalService.ts +250 -0
- package/src/broker/InMemoryBroker.ts +5 -0
- package/src/compression/CompressionUtils.ts +113 -0
- package/src/compression/index.ts +14 -0
- package/src/journal/COALESCING.md +267 -0
- package/src/journal/CoalescingService.ts +626 -0
- package/src/journal/JournalRLE.ts +265 -0
- package/src/journal/JournalService.ts +163 -11
- package/src/journal/RLECompression.ts +210 -0
- package/src/journal/rle-demo.ts +193 -0
- package/src/serve.ts +702 -15
- package/tests/benchmark/journal-optimization-benchmark.test.ts +482 -0
- package/tests/compression/compression.test.ts +343 -0
- package/tests/integration/repositories.test.ts +89 -0
- package/tests/journal/compaction-load-bug.test.ts +409 -0
- package/tests/journal/compaction.test.ts +42 -2
- package/tests/journal/journal-rle.test.ts +511 -0
- package/tests/journal/lww-ordering-bug.test.ts +248 -0
- package/tests/journal/multi-session-coalescing.test.ts +871 -0
- package/tests/journal/rle-compression.test.ts +526 -0
- package/tests/journal/text-coalescing.test.ts +210 -0
- package/tests/unit/s3-compression.test.ts +257 -0
- package/PHASE1_SUMMARY.md +0 -94
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for RLE (Run-Length Encoding) journal compression
|
|
3
|
+
*
|
|
4
|
+
* Tests:
|
|
5
|
+
* 1. RLE encode/decode roundtrip
|
|
6
|
+
* 2. CRDT semantics preservation
|
|
7
|
+
* 3. Compression ratio measurement
|
|
8
|
+
* 4. Edge cases (empty, single message, all same agent, etc.)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from '@jest/globals';
|
|
12
|
+
import type { CRDTMessage } from '@vuer-ai/vuer-rtc';
|
|
13
|
+
import {
|
|
14
|
+
encodeJournalRLE,
|
|
15
|
+
decodeJournalRLE,
|
|
16
|
+
verifyRLEIntegrity,
|
|
17
|
+
getCompressionStats,
|
|
18
|
+
type RLEEncodedJournal,
|
|
19
|
+
} from '../../src/journal/JournalRLE.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Helper: Create a test message
|
|
23
|
+
*/
|
|
24
|
+
function createTestMessage(
|
|
25
|
+
id: string,
|
|
26
|
+
sessionId: string,
|
|
27
|
+
lamportTime: number,
|
|
28
|
+
timestamp: number,
|
|
29
|
+
ops: any[] = []
|
|
30
|
+
): CRDTMessage {
|
|
31
|
+
return {
|
|
32
|
+
id,
|
|
33
|
+
sessionId,
|
|
34
|
+
clock: { [sessionId]: lamportTime },
|
|
35
|
+
lamportTime,
|
|
36
|
+
timestamp,
|
|
37
|
+
ops: ops.length > 0 ? ops : [{
|
|
38
|
+
key: 'cube-1',
|
|
39
|
+
otype: 'vector3.set',
|
|
40
|
+
path: 'position',
|
|
41
|
+
value: [lamportTime, 0, 0],
|
|
42
|
+
}],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('JournalRLE - Run-Length Encoding', () => {
|
|
47
|
+
describe('Basic encode/decode roundtrip', () => {
|
|
48
|
+
it('should encode and decode empty journal', () => {
|
|
49
|
+
const messages: CRDTMessage[] = [];
|
|
50
|
+
const encoded = encodeJournalRLE(messages);
|
|
51
|
+
const decoded = decodeJournalRLE(encoded);
|
|
52
|
+
|
|
53
|
+
expect(decoded).toEqual([]);
|
|
54
|
+
expect(encoded.totalMessages).toBe(0);
|
|
55
|
+
expect(encoded.segments).toHaveLength(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should encode and decode single message', () => {
|
|
59
|
+
const messages = [
|
|
60
|
+
createTestMessage('msg-1', 'session-1', 1, 1000),
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const encoded = encodeJournalRLE(messages);
|
|
64
|
+
const decoded = decodeJournalRLE(encoded);
|
|
65
|
+
|
|
66
|
+
expect(decoded).toEqual(messages);
|
|
67
|
+
expect(encoded.totalMessages).toBe(1);
|
|
68
|
+
expect(encoded.segments).toHaveLength(1);
|
|
69
|
+
expect(encoded.segments[0].agentId).toBe('session-1');
|
|
70
|
+
expect(encoded.segments[0].count).toBe(1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should encode consecutive messages from same agent', () => {
|
|
74
|
+
const messages = [
|
|
75
|
+
createTestMessage('msg-1', 'session-1', 1, 1000),
|
|
76
|
+
createTestMessage('msg-2', 'session-1', 2, 1001),
|
|
77
|
+
createTestMessage('msg-3', 'session-1', 3, 1002),
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const encoded = encodeJournalRLE(messages);
|
|
81
|
+
const decoded = decodeJournalRLE(encoded);
|
|
82
|
+
|
|
83
|
+
expect(decoded).toEqual(messages);
|
|
84
|
+
expect(encoded.totalMessages).toBe(3);
|
|
85
|
+
expect(encoded.segments).toHaveLength(1);
|
|
86
|
+
expect(encoded.segments[0].count).toBe(3);
|
|
87
|
+
expect(encoded.segments[0].messages).toHaveLength(3);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should segment messages from different agents', () => {
|
|
91
|
+
const messages = [
|
|
92
|
+
createTestMessage('msg-1', 'session-1', 1, 1000),
|
|
93
|
+
createTestMessage('msg-2', 'session-1', 2, 1001),
|
|
94
|
+
createTestMessage('msg-3', 'session-2', 3, 1002),
|
|
95
|
+
createTestMessage('msg-4', 'session-2', 4, 1003),
|
|
96
|
+
createTestMessage('msg-5', 'session-1', 5, 1004),
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
const encoded = encodeJournalRLE(messages);
|
|
100
|
+
const decoded = decodeJournalRLE(encoded);
|
|
101
|
+
|
|
102
|
+
expect(decoded).toEqual(messages);
|
|
103
|
+
expect(encoded.totalMessages).toBe(5);
|
|
104
|
+
expect(encoded.segments).toHaveLength(3);
|
|
105
|
+
|
|
106
|
+
// First segment: session-1, 2 messages
|
|
107
|
+
expect(encoded.segments[0].agentId).toBe('session-1');
|
|
108
|
+
expect(encoded.segments[0].count).toBe(2);
|
|
109
|
+
|
|
110
|
+
// Second segment: session-2, 2 messages
|
|
111
|
+
expect(encoded.segments[1].agentId).toBe('session-2');
|
|
112
|
+
expect(encoded.segments[1].count).toBe(2);
|
|
113
|
+
|
|
114
|
+
// Third segment: session-1, 1 message
|
|
115
|
+
expect(encoded.segments[2].agentId).toBe('session-1');
|
|
116
|
+
expect(encoded.segments[2].count).toBe(1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should handle many agents with alternating pattern', () => {
|
|
120
|
+
const messages = Array.from({ length: 10 }, (_, i) => {
|
|
121
|
+
const sessionId = i % 2 === 0 ? 'session-1' : 'session-2';
|
|
122
|
+
return createTestMessage(`msg-${i}`, sessionId, i, 1000 + i);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const encoded = encodeJournalRLE(messages);
|
|
126
|
+
const decoded = decodeJournalRLE(encoded);
|
|
127
|
+
|
|
128
|
+
expect(decoded).toEqual(messages);
|
|
129
|
+
expect(encoded.totalMessages).toBe(10);
|
|
130
|
+
expect(encoded.segments).toHaveLength(10); // Each alternates, so 10 segments
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('CRDT semantics preservation', () => {
|
|
135
|
+
it('should preserve vector clocks', () => {
|
|
136
|
+
const messages = [
|
|
137
|
+
{
|
|
138
|
+
id: 'msg-1',
|
|
139
|
+
sessionId: 'session-1',
|
|
140
|
+
clock: { 'session-1': 5, 'session-2': 3 },
|
|
141
|
+
lamportTime: 8,
|
|
142
|
+
timestamp: 1000,
|
|
143
|
+
ops: [],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'msg-2',
|
|
147
|
+
sessionId: 'session-1',
|
|
148
|
+
clock: { 'session-1': 6, 'session-2': 3 },
|
|
149
|
+
lamportTime: 9,
|
|
150
|
+
timestamp: 1001,
|
|
151
|
+
ops: [],
|
|
152
|
+
},
|
|
153
|
+
] as CRDTMessage[];
|
|
154
|
+
|
|
155
|
+
const encoded = encodeJournalRLE(messages);
|
|
156
|
+
const decoded = decodeJournalRLE(encoded);
|
|
157
|
+
|
|
158
|
+
expect(decoded[0].clock).toEqual({ 'session-1': 5, 'session-2': 3 });
|
|
159
|
+
expect(decoded[1].clock).toEqual({ 'session-1': 6, 'session-2': 3 });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should preserve lamport timestamps', () => {
|
|
163
|
+
const messages = [
|
|
164
|
+
createTestMessage('msg-1', 'session-1', 1, 1000),
|
|
165
|
+
createTestMessage('msg-2', 'session-1', 2, 2000),
|
|
166
|
+
createTestMessage('msg-3', 'session-2', 3, 3000),
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
const encoded = encodeJournalRLE(messages);
|
|
170
|
+
const decoded = decodeJournalRLE(encoded);
|
|
171
|
+
|
|
172
|
+
expect(decoded[0].lamportTime).toBe(1);
|
|
173
|
+
expect(decoded[1].lamportTime).toBe(2);
|
|
174
|
+
expect(decoded[2].lamportTime).toBe(3);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should preserve operation semantics', () => {
|
|
178
|
+
const ops = [
|
|
179
|
+
{ key: 'cube-1', otype: 'vector3.set', path: 'position', value: [1, 2, 3] },
|
|
180
|
+
{ key: 'cube-2', otype: 'vector3.add', path: 'position', value: [0.5, 0.5, 0.5] },
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
const messages = [
|
|
184
|
+
createTestMessage('msg-1', 'session-1', 1, 1000, [ops[0]]),
|
|
185
|
+
createTestMessage('msg-2', 'session-1', 2, 1001, [ops[1]]),
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
const encoded = encodeJournalRLE(messages);
|
|
189
|
+
const decoded = decodeJournalRLE(encoded);
|
|
190
|
+
|
|
191
|
+
expect(JSON.stringify(decoded[0].ops)).toBe(JSON.stringify([ops[0]]));
|
|
192
|
+
expect(JSON.stringify(decoded[1].ops)).toBe(JSON.stringify([ops[1]]));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should verify integrity after roundtrip', () => {
|
|
196
|
+
const messages = [
|
|
197
|
+
createTestMessage('msg-1', 'session-1', 1, 1000),
|
|
198
|
+
createTestMessage('msg-2', 'session-1', 2, 1001),
|
|
199
|
+
createTestMessage('msg-3', 'session-2', 3, 1002),
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
const encoded = encodeJournalRLE(messages);
|
|
203
|
+
const verification = verifyRLEIntegrity(messages, encoded);
|
|
204
|
+
|
|
205
|
+
expect(verification.valid).toBe(true);
|
|
206
|
+
expect(verification.errors).toHaveLength(0);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('Compression ratio measurement', () => {
|
|
211
|
+
it('should compute compression ratio for small journal', () => {
|
|
212
|
+
const messages = [
|
|
213
|
+
createTestMessage('msg-1', 'session-1', 1, 1000),
|
|
214
|
+
createTestMessage('msg-2', 'session-1', 2, 1001),
|
|
215
|
+
createTestMessage('msg-3', 'session-1', 3, 1002),
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
const encoded = encodeJournalRLE(messages);
|
|
219
|
+
const stats = getCompressionStats(encoded);
|
|
220
|
+
|
|
221
|
+
// Same-agent runs reduce redundancy, but JSON overhead is significant
|
|
222
|
+
// RLE is more effective with binary encoding or larger journals
|
|
223
|
+
expect(stats.segmentCount).toBe(1);
|
|
224
|
+
expect(stats.avgMessagesPerSegment).toBeCloseTo(3, 0);
|
|
225
|
+
expect(stats.originalBytes).toBeGreaterThan(0);
|
|
226
|
+
expect(stats.compressedBytes).toBeGreaterThan(0);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should compute compression ratio for large homogeneous journal', () => {
|
|
230
|
+
// 100 messages from same agent
|
|
231
|
+
const messages = Array.from({ length: 100 }, (_, i) =>
|
|
232
|
+
createTestMessage(`msg-${i}`, 'session-1', i, 1000 + i)
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const encoded = encodeJournalRLE(messages);
|
|
236
|
+
const stats = getCompressionStats(encoded);
|
|
237
|
+
|
|
238
|
+
// RLE groups same-agent messages into single segment
|
|
239
|
+
expect(stats.segmentCount).toBe(1);
|
|
240
|
+
expect(stats.avgMessagesPerSegment).toBeCloseTo(100, 0);
|
|
241
|
+
// With JSON encoding, savings are modest; binary codecs would show better ratios
|
|
242
|
+
expect(stats.originalBytes).toBeGreaterThan(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should have low compression for heterogeneous journal', () => {
|
|
246
|
+
// 100 messages alternating between 10 agents
|
|
247
|
+
const messages = Array.from({ length: 100 }, (_, i) => {
|
|
248
|
+
const sessionId = `session-${i % 10}`;
|
|
249
|
+
return createTestMessage(`msg-${i}`, sessionId, i, 1000 + i);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const encoded = encodeJournalRLE(messages);
|
|
253
|
+
const stats = getCompressionStats(encoded);
|
|
254
|
+
|
|
255
|
+
// Many small segments = poor compression (but still valid)
|
|
256
|
+
expect(stats.ratio).toBeLessThan(1.5);
|
|
257
|
+
expect(stats.segmentCount).toBe(100); // Each message is own segment
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should report accurate size metrics', () => {
|
|
261
|
+
const messages = [
|
|
262
|
+
createTestMessage('msg-1', 'session-1', 1, 1000),
|
|
263
|
+
createTestMessage('msg-2', 'session-1', 2, 1001),
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
const encoded = encodeJournalRLE(messages);
|
|
267
|
+
const stats = getCompressionStats(encoded);
|
|
268
|
+
|
|
269
|
+
expect(stats.originalBytes).toBeGreaterThan(0);
|
|
270
|
+
expect(stats.compressedBytes).toBeGreaterThan(0);
|
|
271
|
+
expect(stats.savedBytes).toBe(stats.originalBytes - stats.compressedBytes);
|
|
272
|
+
expect(stats.percentSaved).toBeCloseTo(
|
|
273
|
+
(stats.savedBytes / stats.originalBytes) * 100,
|
|
274
|
+
1
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('Error handling', () => {
|
|
280
|
+
it('should detect decode errors on corrupted data', () => {
|
|
281
|
+
const corrupted: RLEEncodedJournal = {
|
|
282
|
+
version: 1,
|
|
283
|
+
totalMessages: 5,
|
|
284
|
+
segments: [
|
|
285
|
+
{
|
|
286
|
+
agentId: 'session-1',
|
|
287
|
+
count: 5,
|
|
288
|
+
messages: [
|
|
289
|
+
createTestMessage('msg-1', 'session-2', 1, 1000), // Wrong sessionId!
|
|
290
|
+
] as CRDTMessage[],
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
metadata: { compressionRatio: 1, originalSize: 0, compressedSize: 0 },
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
expect(() => decodeJournalRLE(corrupted)).toThrow(
|
|
297
|
+
/sessionId mismatch/
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should detect count mismatch', () => {
|
|
302
|
+
const corrupted: RLEEncodedJournal = {
|
|
303
|
+
version: 1,
|
|
304
|
+
totalMessages: 10, // Claim 10
|
|
305
|
+
segments: [
|
|
306
|
+
{
|
|
307
|
+
agentId: 'session-1',
|
|
308
|
+
count: 2,
|
|
309
|
+
messages: [
|
|
310
|
+
createTestMessage('msg-1', 'session-1', 1, 1000),
|
|
311
|
+
createTestMessage('msg-2', 'session-1', 2, 1001),
|
|
312
|
+
] as CRDTMessage[],
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
metadata: { compressionRatio: 1, originalSize: 0, compressedSize: 0 },
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
expect(() => decodeJournalRLE(corrupted)).toThrow(
|
|
319
|
+
/expected 10 messages.*got 2/
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should report integrity errors', () => {
|
|
324
|
+
const original = [
|
|
325
|
+
createTestMessage('msg-1', 'session-1', 1, 1000),
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
const corrupted: RLEEncodedJournal = {
|
|
329
|
+
version: 1,
|
|
330
|
+
totalMessages: 1,
|
|
331
|
+
segments: [
|
|
332
|
+
{
|
|
333
|
+
agentId: 'session-1',
|
|
334
|
+
count: 1,
|
|
335
|
+
messages: [
|
|
336
|
+
{
|
|
337
|
+
...original[0],
|
|
338
|
+
id: 'msg-corrupted', // Wrong ID
|
|
339
|
+
},
|
|
340
|
+
] as CRDTMessage[],
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
metadata: { compressionRatio: 1, originalSize: 0, compressedSize: 0 },
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const verification = verifyRLEIntegrity(original, corrupted);
|
|
347
|
+
expect(verification.valid).toBe(false);
|
|
348
|
+
expect(verification.errors.length).toBeGreaterThan(0);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe('Edge cases', () => {
|
|
353
|
+
it('should handle messages with complex operations', () => {
|
|
354
|
+
const complexOps = [
|
|
355
|
+
{
|
|
356
|
+
key: 'node-1',
|
|
357
|
+
otype: 'node.insert',
|
|
358
|
+
path: 'children',
|
|
359
|
+
value: {
|
|
360
|
+
key: 'child-1',
|
|
361
|
+
tag: 'Mesh',
|
|
362
|
+
name: 'Cube',
|
|
363
|
+
position: [0, 0, 0],
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
key: '_meta',
|
|
368
|
+
otype: 'meta.undo',
|
|
369
|
+
path: '_meta',
|
|
370
|
+
targetMsgId: 'msg-1',
|
|
371
|
+
},
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
const messages = [
|
|
375
|
+
createTestMessage('msg-1', 'session-1', 1, 1000, [complexOps[0]]),
|
|
376
|
+
createTestMessage('msg-2', 'session-1', 2, 1001, [complexOps[1]]),
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
const encoded = encodeJournalRLE(messages);
|
|
380
|
+
const decoded = decodeJournalRLE(encoded);
|
|
381
|
+
const verification = verifyRLEIntegrity(messages, encoded);
|
|
382
|
+
|
|
383
|
+
expect(decoded).toEqual(messages);
|
|
384
|
+
expect(verification.valid).toBe(true);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should handle multiple operations per message', () => {
|
|
388
|
+
const multiOpMsg: CRDTMessage = {
|
|
389
|
+
id: 'msg-1',
|
|
390
|
+
sessionId: 'session-1',
|
|
391
|
+
clock: { 'session-1': 1 },
|
|
392
|
+
lamportTime: 1,
|
|
393
|
+
timestamp: 1000,
|
|
394
|
+
ops: [
|
|
395
|
+
{ key: 'node-1', otype: 'vector3.set', path: 'position', value: [1, 2, 3] },
|
|
396
|
+
{ key: 'node-2', otype: 'vector3.set', path: 'position', value: [4, 5, 6] },
|
|
397
|
+
{ key: 'node-3', otype: 'vector3.set', path: 'position', value: [7, 8, 9] },
|
|
398
|
+
],
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const messages = [multiOpMsg];
|
|
402
|
+
const encoded = encodeJournalRLE(messages);
|
|
403
|
+
const decoded = decodeJournalRLE(encoded);
|
|
404
|
+
const verification = verifyRLEIntegrity(messages, encoded);
|
|
405
|
+
|
|
406
|
+
expect(decoded[0].ops).toHaveLength(3);
|
|
407
|
+
expect(verification.valid).toBe(true);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should handle large timestamps', () => {
|
|
411
|
+
const largeTimestamp = Number.MAX_SAFE_INTEGER / 1000;
|
|
412
|
+
const messages = [
|
|
413
|
+
createTestMessage('msg-1', 'session-1', 1, largeTimestamp),
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
const encoded = encodeJournalRLE(messages);
|
|
417
|
+
const decoded = decodeJournalRLE(encoded);
|
|
418
|
+
|
|
419
|
+
expect(decoded[0].timestamp).toBe(largeTimestamp);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
describe('Real-world scenarios', () => {
|
|
424
|
+
it('should compress a realistic editing session', () => {
|
|
425
|
+
// Simulate: User A does 10 edits, then User B does 5, then A does 10 more
|
|
426
|
+
const messages: CRDTMessage[] = [];
|
|
427
|
+
|
|
428
|
+
// User A: 10 edits
|
|
429
|
+
for (let i = 0; i < 10; i++) {
|
|
430
|
+
messages.push(
|
|
431
|
+
createTestMessage(
|
|
432
|
+
`msg-a-${i}`,
|
|
433
|
+
'session-a',
|
|
434
|
+
i,
|
|
435
|
+
1000 + i,
|
|
436
|
+
[{ key: 'cube-1', otype: 'vector3.add', path: 'position', value: [0.1, 0, 0] }]
|
|
437
|
+
)
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// User B: 5 edits
|
|
442
|
+
for (let i = 0; i < 5; i++) {
|
|
443
|
+
messages.push(
|
|
444
|
+
createTestMessage(
|
|
445
|
+
`msg-b-${i}`,
|
|
446
|
+
'session-b',
|
|
447
|
+
i,
|
|
448
|
+
1010 + i,
|
|
449
|
+
[{ key: 'cube-2', otype: 'vector3.add', path: 'position', value: [0, 0.1, 0] }]
|
|
450
|
+
)
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// User A: 10 more edits
|
|
455
|
+
for (let i = 10; i < 20; i++) {
|
|
456
|
+
messages.push(
|
|
457
|
+
createTestMessage(
|
|
458
|
+
`msg-a-${i}`,
|
|
459
|
+
'session-a',
|
|
460
|
+
i,
|
|
461
|
+
1020 + i,
|
|
462
|
+
[{ key: 'cube-1', otype: 'vector3.add', path: 'position', value: [0.1, 0, 0] }]
|
|
463
|
+
)
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const encoded = encodeJournalRLE(messages);
|
|
468
|
+
const stats = getCompressionStats(encoded);
|
|
469
|
+
|
|
470
|
+
expect(encoded.segments).toHaveLength(3);
|
|
471
|
+
// RLE reduces redundancy by grouping same-agent messages
|
|
472
|
+
// Real compression gains manifest with binary codecs or network serialization
|
|
473
|
+
expect(stats.segmentCount).toBe(3);
|
|
474
|
+
|
|
475
|
+
const verification = verifyRLEIntegrity(messages, encoded);
|
|
476
|
+
expect(verification.valid).toBe(true);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('should compress collaborative session with concurrent edits', () => {
|
|
480
|
+
// Simulate: 5 users concurrently editing
|
|
481
|
+
const messages: CRDTMessage[] = [];
|
|
482
|
+
let msgId = 0;
|
|
483
|
+
let lamportTime = 0;
|
|
484
|
+
|
|
485
|
+
for (let round = 0; round < 10; round++) {
|
|
486
|
+
for (let user = 1; user <= 5; user++) {
|
|
487
|
+
messages.push(
|
|
488
|
+
createTestMessage(
|
|
489
|
+
`msg-${msgId++}`,
|
|
490
|
+
`session-${user}`,
|
|
491
|
+
lamportTime++,
|
|
492
|
+
1000 + round * 5 + user
|
|
493
|
+
)
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const encoded = encodeJournalRLE(messages);
|
|
499
|
+
const stats = getCompressionStats(encoded);
|
|
500
|
+
|
|
501
|
+
// Many concurrent users = many segments (alternating pattern)
|
|
502
|
+
expect(encoded.segments.length).toBeGreaterThan(1);
|
|
503
|
+
// JSON overhead dominates with many small segments
|
|
504
|
+
// Real benefit appears with binary protocols or storage backends
|
|
505
|
+
expect(stats.segmentCount).toBeGreaterThan(1);
|
|
506
|
+
|
|
507
|
+
const verification = verifyRLEIntegrity(messages, encoded);
|
|
508
|
+
expect(verification.valid).toBe(true);
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
});
|