@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.
Files changed (76) hide show
  1. package/.env +1 -0
  2. package/S3_COMPRESSION_GUIDE.md +233 -0
  3. package/dist/archive/ArchivalService.d.ts +117 -0
  4. package/dist/archive/ArchivalService.d.ts.map +1 -0
  5. package/dist/archive/ArchivalService.js +181 -0
  6. package/dist/archive/ArchivalService.js.map +1 -0
  7. package/dist/broker/InMemoryBroker.d.ts +2 -0
  8. package/dist/broker/InMemoryBroker.d.ts.map +1 -1
  9. package/dist/broker/InMemoryBroker.js +4 -0
  10. package/dist/broker/InMemoryBroker.js.map +1 -1
  11. package/dist/compression/CompressionUtils.d.ts +57 -0
  12. package/dist/compression/CompressionUtils.d.ts.map +1 -0
  13. package/dist/compression/CompressionUtils.js +90 -0
  14. package/dist/compression/CompressionUtils.js.map +1 -0
  15. package/dist/compression/index.d.ts +7 -0
  16. package/dist/compression/index.d.ts.map +1 -0
  17. package/dist/compression/index.js +7 -0
  18. package/dist/compression/index.js.map +1 -0
  19. package/dist/journal/CoalescingService.d.ts +63 -0
  20. package/dist/journal/CoalescingService.d.ts.map +1 -0
  21. package/dist/journal/CoalescingService.js +507 -0
  22. package/dist/journal/CoalescingService.js.map +1 -0
  23. package/dist/journal/JournalRLE.d.ts +81 -0
  24. package/dist/journal/JournalRLE.d.ts.map +1 -0
  25. package/dist/journal/JournalRLE.js +199 -0
  26. package/dist/journal/JournalRLE.js.map +1 -0
  27. package/dist/journal/JournalService.d.ts +7 -3
  28. package/dist/journal/JournalService.d.ts.map +1 -1
  29. package/dist/journal/JournalService.js +152 -12
  30. package/dist/journal/JournalService.js.map +1 -1
  31. package/dist/journal/RLECompression.d.ts +73 -0
  32. package/dist/journal/RLECompression.d.ts.map +1 -0
  33. package/dist/journal/RLECompression.js +152 -0
  34. package/dist/journal/RLECompression.js.map +1 -0
  35. package/dist/journal/rle-demo.d.ts +8 -0
  36. package/dist/journal/rle-demo.d.ts.map +1 -0
  37. package/dist/journal/rle-demo.js +159 -0
  38. package/dist/journal/rle-demo.js.map +1 -0
  39. package/dist/persistence/S3ColdStorage.d.ts +62 -0
  40. package/dist/persistence/S3ColdStorage.d.ts.map +1 -0
  41. package/dist/persistence/S3ColdStorage.js +88 -0
  42. package/dist/persistence/S3ColdStorage.js.map +1 -0
  43. package/dist/persistence/S3ColdStorageIntegration.d.ts +78 -0
  44. package/dist/persistence/S3ColdStorageIntegration.d.ts.map +1 -0
  45. package/dist/persistence/S3ColdStorageIntegration.js +93 -0
  46. package/dist/persistence/S3ColdStorageIntegration.js.map +1 -0
  47. package/dist/serve.d.ts +2 -0
  48. package/dist/serve.d.ts.map +1 -1
  49. package/dist/serve.js +623 -15
  50. package/dist/serve.js.map +1 -1
  51. package/docs/RLE_COMPRESSION.md +397 -0
  52. package/examples/compression-example.ts +259 -0
  53. package/package.json +14 -14
  54. package/src/archive/ArchivalService.ts +250 -0
  55. package/src/broker/InMemoryBroker.ts +5 -0
  56. package/src/compression/CompressionUtils.ts +113 -0
  57. package/src/compression/index.ts +14 -0
  58. package/src/journal/COALESCING.md +267 -0
  59. package/src/journal/CoalescingService.ts +626 -0
  60. package/src/journal/JournalRLE.ts +265 -0
  61. package/src/journal/JournalService.ts +163 -11
  62. package/src/journal/RLECompression.ts +210 -0
  63. package/src/journal/rle-demo.ts +193 -0
  64. package/src/serve.ts +702 -15
  65. package/tests/benchmark/journal-optimization-benchmark.test.ts +482 -0
  66. package/tests/compression/compression.test.ts +343 -0
  67. package/tests/integration/repositories.test.ts +89 -0
  68. package/tests/journal/compaction-load-bug.test.ts +409 -0
  69. package/tests/journal/compaction.test.ts +42 -2
  70. package/tests/journal/journal-rle.test.ts +511 -0
  71. package/tests/journal/lww-ordering-bug.test.ts +248 -0
  72. package/tests/journal/multi-session-coalescing.test.ts +871 -0
  73. package/tests/journal/rle-compression.test.ts +526 -0
  74. package/tests/journal/text-coalescing.test.ts +210 -0
  75. package/tests/unit/s3-compression.test.ts +257 -0
  76. 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
+ });