@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,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text CRDT Coalescing Test
|
|
3
|
+
*
|
|
4
|
+
* Verifies that compaction merges single-char TextRope items into
|
|
5
|
+
* multi-char spans, preventing B-tree depth explosion.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
|
9
|
+
import type { CRDTMessage, Operation, TextRope } from '@vuer-ai/vuer-rtc';
|
|
10
|
+
import { JournalService } from '../../src/journal/JournalService.js';
|
|
11
|
+
import { PrismaClient } from '@prisma/client';
|
|
12
|
+
import { getItems } from '@vuer-ai/vuer-rtc';
|
|
13
|
+
|
|
14
|
+
describe('TextRope Coalescing in Compaction', () => {
|
|
15
|
+
let service: JournalService;
|
|
16
|
+
let prisma: PrismaClient;
|
|
17
|
+
let documentId: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
prisma = new PrismaClient();
|
|
21
|
+
await prisma.$connect();
|
|
22
|
+
|
|
23
|
+
// Clean up test data
|
|
24
|
+
await prisma.journalBatch.deleteMany({});
|
|
25
|
+
await prisma.document.deleteMany({});
|
|
26
|
+
|
|
27
|
+
service = new JournalService(prisma);
|
|
28
|
+
documentId = await service.createDocument('test-doc', 'test-user');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
await prisma.$disconnect();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should coalesce single-char text inserts into multi-char spans after compaction', async () => {
|
|
36
|
+
// Create text node
|
|
37
|
+
const initMsg: CRDTMessage = {
|
|
38
|
+
id: 'msg-init',
|
|
39
|
+
sessionId: 'alice',
|
|
40
|
+
clock: { alice: 1 },
|
|
41
|
+
lamportTime: 1,
|
|
42
|
+
timestamp: Date.now() / 1000,
|
|
43
|
+
ops: [
|
|
44
|
+
{
|
|
45
|
+
key: 'default-scene',
|
|
46
|
+
otype: 'node.insert',
|
|
47
|
+
path: 'children',
|
|
48
|
+
value: {
|
|
49
|
+
key: 'text-doc',
|
|
50
|
+
tag: 'Text',
|
|
51
|
+
name: 'Text Doc',
|
|
52
|
+
},
|
|
53
|
+
} as Operation,
|
|
54
|
+
{
|
|
55
|
+
key: 'text-doc',
|
|
56
|
+
otype: 'text.init',
|
|
57
|
+
path: 'content',
|
|
58
|
+
value: '',
|
|
59
|
+
} as Operation,
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
await service.processMessage(documentId, initMsg);
|
|
64
|
+
|
|
65
|
+
// Type "hello" character by character (simulating rapid typing)
|
|
66
|
+
const chars = ['h', 'e', 'l', 'l', 'o'];
|
|
67
|
+
for (let i = 0; i < chars.length; i++) {
|
|
68
|
+
const msg: CRDTMessage = {
|
|
69
|
+
id: `msg-char-${i}`,
|
|
70
|
+
sessionId: 'alice',
|
|
71
|
+
clock: { alice: 2 + i },
|
|
72
|
+
lamportTime: 2 + i,
|
|
73
|
+
timestamp: Date.now() / 1000,
|
|
74
|
+
ops: [
|
|
75
|
+
{
|
|
76
|
+
key: 'text-doc',
|
|
77
|
+
otype: 'text.insert',
|
|
78
|
+
path: 'content',
|
|
79
|
+
position: i,
|
|
80
|
+
value: chars[i],
|
|
81
|
+
} as Operation,
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
await service.processMessage(documentId, msg);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Before compaction: get state and check TextRope structure
|
|
88
|
+
const stateBefore = await service.getStateForClient(documentId);
|
|
89
|
+
expect(stateBefore).not.toBeNull();
|
|
90
|
+
|
|
91
|
+
const graphBefore = service.computeGraph((stateBefore as any).snapshot);
|
|
92
|
+
const nodeBefore = graphBefore.nodes['text-doc'];
|
|
93
|
+
const ropeBefore = (nodeBefore as any)['_textRope.content'] as TextRope;
|
|
94
|
+
|
|
95
|
+
expect(ropeBefore).toBeDefined();
|
|
96
|
+
const itemsBefore = getItems(ropeBefore);
|
|
97
|
+
console.log(`Before compaction: ${itemsBefore.length} items`);
|
|
98
|
+
|
|
99
|
+
// Each character should be a separate item
|
|
100
|
+
expect(itemsBefore.length).toBeGreaterThanOrEqual(5);
|
|
101
|
+
|
|
102
|
+
// Compact
|
|
103
|
+
await service.compact(documentId);
|
|
104
|
+
|
|
105
|
+
// After compaction: check TextRope structure
|
|
106
|
+
const stateAfter = await service.getStateForClient(documentId);
|
|
107
|
+
expect(stateAfter).not.toBeNull();
|
|
108
|
+
|
|
109
|
+
const graphAfter = service.computeGraph((stateAfter as any).snapshot);
|
|
110
|
+
const nodeAfter = graphAfter.nodes['text-doc'];
|
|
111
|
+
const ropeAfter = (nodeAfter as any)['_textRope.content'] as TextRope;
|
|
112
|
+
|
|
113
|
+
expect(ropeAfter).toBeDefined();
|
|
114
|
+
const itemsAfter = getItems(ropeAfter);
|
|
115
|
+
console.log(`After compaction: ${itemsAfter.length} items`);
|
|
116
|
+
|
|
117
|
+
// Should be coalesced into 1 or fewer items
|
|
118
|
+
expect(itemsAfter.length).toBeLessThan(itemsBefore.length);
|
|
119
|
+
expect(itemsAfter.length).toBeLessThanOrEqual(1);
|
|
120
|
+
|
|
121
|
+
// Content should still be correct
|
|
122
|
+
const textAfter = nodeAfter.content;
|
|
123
|
+
expect(textAfter).toBe('hello');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should handle large text documents efficiently', async () => {
|
|
127
|
+
// Create text node
|
|
128
|
+
const initMsg: CRDTMessage = {
|
|
129
|
+
id: 'msg-init',
|
|
130
|
+
sessionId: 'alice',
|
|
131
|
+
clock: { alice: 1 },
|
|
132
|
+
lamportTime: 1,
|
|
133
|
+
timestamp: Date.now() / 1000,
|
|
134
|
+
ops: [
|
|
135
|
+
{
|
|
136
|
+
key: 'default-scene',
|
|
137
|
+
otype: 'node.insert',
|
|
138
|
+
path: 'children',
|
|
139
|
+
value: {
|
|
140
|
+
key: 'text-doc',
|
|
141
|
+
tag: 'Text',
|
|
142
|
+
name: 'Text Doc',
|
|
143
|
+
},
|
|
144
|
+
} as Operation,
|
|
145
|
+
{
|
|
146
|
+
key: 'text-doc',
|
|
147
|
+
otype: 'text.init',
|
|
148
|
+
path: 'content',
|
|
149
|
+
value: '',
|
|
150
|
+
} as Operation,
|
|
151
|
+
],
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
await service.processMessage(documentId, initMsg);
|
|
155
|
+
|
|
156
|
+
// Type 100 characters
|
|
157
|
+
const text = 'a'.repeat(100);
|
|
158
|
+
for (let i = 0; i < 100; i++) {
|
|
159
|
+
const msg: CRDTMessage = {
|
|
160
|
+
id: `msg-char-${i}`,
|
|
161
|
+
sessionId: 'alice',
|
|
162
|
+
clock: { alice: 2 + i },
|
|
163
|
+
lamportTime: 2 + i,
|
|
164
|
+
timestamp: Date.now() / 1000,
|
|
165
|
+
ops: [
|
|
166
|
+
{
|
|
167
|
+
key: 'text-doc',
|
|
168
|
+
otype: 'text.insert',
|
|
169
|
+
path: 'content',
|
|
170
|
+
position: i,
|
|
171
|
+
value: text[i],
|
|
172
|
+
} as Operation,
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
await service.processMessage(documentId, msg);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Before compaction
|
|
179
|
+
const stateBefore = await service.getStateForClient(documentId);
|
|
180
|
+
const graphBefore = service.computeGraph((stateBefore as any).snapshot);
|
|
181
|
+
const nodeBefore = graphBefore.nodes['text-doc'];
|
|
182
|
+
const ropeBefore = (nodeBefore as any)['_textRope.content'] as TextRope;
|
|
183
|
+
const itemsBefore = getItems(ropeBefore);
|
|
184
|
+
|
|
185
|
+
console.log(`Before compaction: ${itemsBefore.length} items for 100 chars`);
|
|
186
|
+
expect(itemsBefore.length).toBeGreaterThanOrEqual(100);
|
|
187
|
+
|
|
188
|
+
// Compact
|
|
189
|
+
await service.compact(documentId);
|
|
190
|
+
|
|
191
|
+
// After compaction
|
|
192
|
+
const stateAfter = await service.getStateForClient(documentId);
|
|
193
|
+
const graphAfter = service.computeGraph((stateAfter as any).snapshot);
|
|
194
|
+
const nodeAfter = graphAfter.nodes['text-doc'];
|
|
195
|
+
const ropeAfter = (nodeAfter as any)['_textRope.content'] as TextRope;
|
|
196
|
+
const itemsAfter = getItems(ropeAfter);
|
|
197
|
+
|
|
198
|
+
const reductionPercent = Math.round((1 - itemsAfter.length / itemsBefore.length) * 100);
|
|
199
|
+
console.log(`After compaction: ${itemsAfter.length} items for 100 chars`);
|
|
200
|
+
console.log(`Reduction: ${itemsBefore.length}x → ${itemsAfter.length}x (${reductionPercent}% smaller)`);
|
|
201
|
+
|
|
202
|
+
// Should be significantly reduced
|
|
203
|
+
expect(itemsAfter.length).toBeLessThan(itemsBefore.length * 0.2); // At least 80% reduction
|
|
204
|
+
expect(itemsAfter.length).toBeLessThanOrEqual(1);
|
|
205
|
+
|
|
206
|
+
// Content should still be correct
|
|
207
|
+
const textAfter = nodeAfter.content;
|
|
208
|
+
expect(textAfter).toBe(text);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3 Cold Storage Compression Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests:
|
|
5
|
+
* - Unit test: compress snapshot, verify decompression matches original
|
|
6
|
+
* - Benchmark test: compression ratio on sample data
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect } from '@jest/globals';
|
|
10
|
+
import {
|
|
11
|
+
compressSnapshot,
|
|
12
|
+
decompressSnapshot,
|
|
13
|
+
getCompressionStats,
|
|
14
|
+
} from '../../src/compression/CompressionUtils';
|
|
15
|
+
import type { Snapshot } from '@vuer-ai/vuer-rtc';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Helper to format bytes as human-readable string
|
|
19
|
+
*/
|
|
20
|
+
function formatBytes(bytes: number): string {
|
|
21
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
22
|
+
let size = bytes;
|
|
23
|
+
let unitIndex = 0;
|
|
24
|
+
|
|
25
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
26
|
+
size /= 1024;
|
|
27
|
+
unitIndex += 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Calculate compression ratio
|
|
35
|
+
*/
|
|
36
|
+
function calculateCompressionRatio(
|
|
37
|
+
originalSize: number,
|
|
38
|
+
compressedSize: number
|
|
39
|
+
): { ratio: number; savings: string } {
|
|
40
|
+
const ratio = compressedSize / originalSize;
|
|
41
|
+
const savingsPercent = ((1 - ratio) * 100).toFixed(1);
|
|
42
|
+
return {
|
|
43
|
+
ratio,
|
|
44
|
+
savings: `${savingsPercent}%`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a test snapshot with variable complexity
|
|
50
|
+
*/
|
|
51
|
+
function createTestSnapshot(nodeCount: number = 100, depth: number = 3): Snapshot {
|
|
52
|
+
const nodes: Record<string, any> = {};
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < nodeCount; i++) {
|
|
55
|
+
nodes[`node_${i}`] = {
|
|
56
|
+
id: `node_${i}`,
|
|
57
|
+
name: `Test Node ${i}`,
|
|
58
|
+
type: 'mesh',
|
|
59
|
+
position: [Math.random() * 100, Math.random() * 100, Math.random() * 100],
|
|
60
|
+
rotation: [0, 0, 0, 1],
|
|
61
|
+
scale: [1, 1, 1],
|
|
62
|
+
visible: true,
|
|
63
|
+
metadata: {
|
|
64
|
+
createdAt: Date.now(),
|
|
65
|
+
tags: ['test', 'benchmark'],
|
|
66
|
+
description: `This is a test node with index ${i}`.repeat(5), // Repeat for more data
|
|
67
|
+
},
|
|
68
|
+
children: Array.from({ length: depth }, (_, j) => `node_${i}_child_${j}`),
|
|
69
|
+
properties: {
|
|
70
|
+
color: [Math.random(), Math.random(), Math.random()],
|
|
71
|
+
emissive: [0, 0, 0],
|
|
72
|
+
roughness: 0.5,
|
|
73
|
+
metalness: 0.5,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
graph: {
|
|
80
|
+
rootKey: 'scene_root',
|
|
81
|
+
nodes,
|
|
82
|
+
},
|
|
83
|
+
vectorClock: {
|
|
84
|
+
session1: 100,
|
|
85
|
+
session2: 50,
|
|
86
|
+
session3: 75,
|
|
87
|
+
},
|
|
88
|
+
lamportTime: 150,
|
|
89
|
+
journalIndex: 200,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe('S3 Cold Storage - Compression', () => {
|
|
94
|
+
describe('Unit Tests', () => {
|
|
95
|
+
it('should compress and decompress a snapshot correctly', () => {
|
|
96
|
+
// Create test snapshot
|
|
97
|
+
const originalSnapshot = createTestSnapshot(50, 2);
|
|
98
|
+
|
|
99
|
+
// Compress
|
|
100
|
+
const compressed = compressSnapshot(originalSnapshot);
|
|
101
|
+
|
|
102
|
+
// Verify compression produced a buffer
|
|
103
|
+
expect(compressed.compressed).toBeInstanceOf(Buffer);
|
|
104
|
+
expect(compressed.originalSize).toBeGreaterThan(0);
|
|
105
|
+
expect(compressed.compressedSize).toBeGreaterThan(0);
|
|
106
|
+
expect(compressed.ratio).toBeGreaterThan(0); // Ratio is percentage
|
|
107
|
+
|
|
108
|
+
// Decompress
|
|
109
|
+
const decompressed = decompressSnapshot(compressed.compressed);
|
|
110
|
+
|
|
111
|
+
// Verify decompressed matches original
|
|
112
|
+
const decompressedSnapshot = JSON.parse(decompressed.decompressed.toString('utf-8'));
|
|
113
|
+
expect(decompressedSnapshot).toEqual(originalSnapshot);
|
|
114
|
+
expect(decompressed.verified).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should handle large snapshots', () => {
|
|
118
|
+
// Create a larger snapshot
|
|
119
|
+
const largeSnapshot = createTestSnapshot(1000, 5);
|
|
120
|
+
|
|
121
|
+
// Compress
|
|
122
|
+
const compressed = compressSnapshot(largeSnapshot);
|
|
123
|
+
|
|
124
|
+
// Should achieve good compression (expect ~75-80% savings)
|
|
125
|
+
const savingsPercent = 100 - compressed.ratio;
|
|
126
|
+
expect(savingsPercent).toBeGreaterThan(50); // At least 50% savings
|
|
127
|
+
|
|
128
|
+
// Decompress and verify
|
|
129
|
+
const decompressed = decompressSnapshot(compressed.compressed);
|
|
130
|
+
const decompressedSnapshot = JSON.parse(decompressed.decompressed.toString('utf-8'));
|
|
131
|
+
expect(decompressedSnapshot).toEqual(largeSnapshot);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should handle empty snapshots', () => {
|
|
135
|
+
const emptySnapshot: Snapshot = {
|
|
136
|
+
graph: { rootKey: '', nodes: {} },
|
|
137
|
+
vectorClock: {},
|
|
138
|
+
lamportTime: 0,
|
|
139
|
+
journalIndex: 0,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Compress
|
|
143
|
+
const compressed = compressSnapshot(emptySnapshot);
|
|
144
|
+
expect(compressed.compressed).toBeInstanceOf(Buffer);
|
|
145
|
+
|
|
146
|
+
// Decompress and verify
|
|
147
|
+
const decompressed = decompressSnapshot(compressed.compressed);
|
|
148
|
+
const decompressedSnapshot = JSON.parse(decompressed.decompressed.toString('utf-8'));
|
|
149
|
+
expect(decompressedSnapshot).toEqual(emptySnapshot);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should provide accurate compression metrics', () => {
|
|
153
|
+
const snapshot = createTestSnapshot(100, 3);
|
|
154
|
+
const compressed = compressSnapshot(snapshot);
|
|
155
|
+
|
|
156
|
+
// Verify metrics
|
|
157
|
+
expect(compressed.originalSize).toBeGreaterThan(0);
|
|
158
|
+
expect(compressed.compressedSize).toBeGreaterThan(0);
|
|
159
|
+
expect(compressed.ratio).toBe((compressed.compressedSize / compressed.originalSize) * 100);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should reject corrupted compressed data', () => {
|
|
163
|
+
const corruptedData = Buffer.from('not valid gzip data');
|
|
164
|
+
|
|
165
|
+
// Should throw error on decompression
|
|
166
|
+
expect(() => decompressSnapshot(corruptedData)).toThrow();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('Benchmark Tests', () => {
|
|
171
|
+
it('should compress typical snapshots with 75-80% savings', () => {
|
|
172
|
+
// Create a realistic snapshot
|
|
173
|
+
const snapshot = createTestSnapshot(500, 4);
|
|
174
|
+
|
|
175
|
+
const compressed = compressSnapshot(snapshot);
|
|
176
|
+
const savingsPercent = 100 - compressed.ratio;
|
|
177
|
+
|
|
178
|
+
console.log('\nCompression Benchmark Results:');
|
|
179
|
+
console.log(` Original size: ${formatBytes(compressed.originalSize)}`);
|
|
180
|
+
console.log(` Compressed size: ${formatBytes(compressed.compressedSize)}`);
|
|
181
|
+
console.log(` Savings: ${savingsPercent.toFixed(1)}%`);
|
|
182
|
+
console.log(` Compression ratio: ${(compressed.ratio / 100).toFixed(4)}`);
|
|
183
|
+
|
|
184
|
+
// Verify benchmark expectations
|
|
185
|
+
expect(savingsPercent).toBeGreaterThan(70);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should handle compression ratio scaling with snapshot size', () => {
|
|
189
|
+
const results: {
|
|
190
|
+
size: string;
|
|
191
|
+
original: number;
|
|
192
|
+
compressed: number;
|
|
193
|
+
ratio: number;
|
|
194
|
+
savings: string;
|
|
195
|
+
}[] = [];
|
|
196
|
+
|
|
197
|
+
// Test different snapshot sizes
|
|
198
|
+
for (const nodeCount of [100, 500, 1000]) {
|
|
199
|
+
const snapshot = createTestSnapshot(nodeCount, 4);
|
|
200
|
+
const compressed = compressSnapshot(snapshot);
|
|
201
|
+
const { ratio, savings } = calculateCompressionRatio(
|
|
202
|
+
compressed.originalSize,
|
|
203
|
+
compressed.compressedSize
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
results.push({
|
|
207
|
+
size: `${nodeCount} nodes`,
|
|
208
|
+
original: compressed.originalSize,
|
|
209
|
+
compressed: compressed.compressedSize,
|
|
210
|
+
ratio,
|
|
211
|
+
savings,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
console.log('\nCompression Ratio vs Snapshot Size:');
|
|
216
|
+
for (const r of results) {
|
|
217
|
+
console.log(
|
|
218
|
+
` ${r.size}: ${formatBytes(r.original)} to ${formatBytes(r.compressed)} (${r.savings})`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// All should have good compression
|
|
223
|
+
for (const r of results) {
|
|
224
|
+
expect(parseFloat(r.savings)).toBeGreaterThan(60);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('Utility Functions', () => {
|
|
230
|
+
it('should calculate compression ratio correctly', () => {
|
|
231
|
+
const original = 1000;
|
|
232
|
+
const compressed = 250;
|
|
233
|
+
|
|
234
|
+
const result = calculateCompressionRatio(original, compressed);
|
|
235
|
+
|
|
236
|
+
expect(result.ratio).toBe(0.25);
|
|
237
|
+
expect(result.savings).toBe('75.0%');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should format bytes correctly', () => {
|
|
241
|
+
expect(formatBytes(0)).toBe('0.00 B');
|
|
242
|
+
expect(formatBytes(1024)).toBe('1.00 KB');
|
|
243
|
+
expect(formatBytes(1024 * 1024)).toBe('1.00 MB');
|
|
244
|
+
expect(formatBytes(1024 * 1024 * 1024)).toBe('1.00 GB');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should get compression stats correctly', () => {
|
|
248
|
+
const snapshot = createTestSnapshot(100, 3);
|
|
249
|
+
const stats = getCompressionStats(snapshot);
|
|
250
|
+
|
|
251
|
+
expect(stats.originalSize).toBeGreaterThan(0);
|
|
252
|
+
expect(stats.compressedSize).toBeGreaterThan(0);
|
|
253
|
+
expect(stats.ratio).toBeGreaterThan(0);
|
|
254
|
+
expect(stats.savedBytes).toBeGreaterThan(0);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
package/PHASE1_SUMMARY.md
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# Phase 1: Foundation - Completed ✅
|
|
2
|
-
|
|
3
|
-
## Summary
|
|
4
|
-
Successfully implemented the foundation for Vuer RTC with comprehensive test coverage using Test-Driven Development (TDD).
|
|
5
|
-
|
|
6
|
-
## What Was Built
|
|
7
|
-
|
|
8
|
-
### 1. Testing Infrastructure
|
|
9
|
-
- **Jest** with TypeScript and ESM support
|
|
10
|
-
- Parallel test execution (50% of available CPUs)
|
|
11
|
-
- Async test support for integration tests
|
|
12
|
-
- Test directory structure: `unit/`, `integration/`, `e2e/`
|
|
13
|
-
- **Test Coverage**: 45 passing unit tests
|
|
14
|
-
|
|
15
|
-
### 2. Vector Clock Implementation (CRDT)
|
|
16
|
-
- `VectorClockManager` class with full CRDT support
|
|
17
|
-
- Operations: create, increment, merge, compare
|
|
18
|
-
- Causal ordering detection
|
|
19
|
-
- Concurrent operation detection
|
|
20
|
-
- **23 tests** covering all edge cases
|
|
21
|
-
|
|
22
|
-
### 3. Operation Types & Validation
|
|
23
|
-
- **4 operation types**: CREATE, UPDATE, DELETE, TRANSFORM
|
|
24
|
-
- Complete TypeScript type definitions
|
|
25
|
-
- `OperationValidator` with schema validation
|
|
26
|
-
- Transform3D with quaternion rotation support
|
|
27
|
-
- **20 tests** for type guards and validation
|
|
28
|
-
|
|
29
|
-
### 4. Prisma Schema (MongoDB)
|
|
30
|
-
- **5 data models**: Document, SceneObject, Operation, JournalBatch, Session
|
|
31
|
-
- CRDT metadata for conflict resolution
|
|
32
|
-
- Soft deletes with tombstone pattern
|
|
33
|
-
- Optimized indexes for queries
|
|
34
|
-
- Write-ahead log structure (33ms batching)
|
|
35
|
-
|
|
36
|
-
### 5. Database Repositories
|
|
37
|
-
- `PrismaClient` singleton for connection management
|
|
38
|
-
- `DocumentRepository` with CRUD operations
|
|
39
|
-
- `SessionRepository` with presence tracking
|
|
40
|
-
- Version incrementing and clock value management
|
|
41
|
-
- Integration tests ready (require MongoDB)
|
|
42
|
-
|
|
43
|
-
### 6. Code Quality
|
|
44
|
-
- Modular architecture with clean exports
|
|
45
|
-
- High-level abstractions
|
|
46
|
-
- Comprehensive documentation
|
|
47
|
-
- Type-safe implementations
|
|
48
|
-
|
|
49
|
-
## Git Commits
|
|
50
|
-
|
|
51
|
-
1. `19da4ca` - Initialize Jest testing framework
|
|
52
|
-
2. `4cfcd90` - Implement vector clock with tests (23 tests)
|
|
53
|
-
3. `6ab7c9e` - Add operation types and validation (20 tests)
|
|
54
|
-
4. `ce90de3` - Add Prisma schema for all models
|
|
55
|
-
5. `35920c6` - Add database repositories
|
|
56
|
-
6. `b2a81a8` - Refactor with modular exports
|
|
57
|
-
|
|
58
|
-
## Metrics
|
|
59
|
-
|
|
60
|
-
- **Total Tests**: 45 passing
|
|
61
|
-
- **Code Coverage**: Ready for >80% threshold
|
|
62
|
-
- **TypeScript**: 100% type-safe
|
|
63
|
-
- **MongoDB Models**: 5 complete schemas
|
|
64
|
-
- **Repository Methods**: 15+ database operations
|
|
65
|
-
|
|
66
|
-
## Ready for Next Phase
|
|
67
|
-
|
|
68
|
-
Phase 1 provides the complete foundation for:
|
|
69
|
-
- Phase 2: State Management (SceneState, StateManager, ConflictResolver)
|
|
70
|
-
- Phase 3: WebSocket Server (real-time communication)
|
|
71
|
-
- Phase 4: Write-Ahead Log (journal with batching)
|
|
72
|
-
- Phase 5: History/Undo system
|
|
73
|
-
- Phase 6: Client Library
|
|
74
|
-
- Phase 7: Documentation site
|
|
75
|
-
|
|
76
|
-
## To Create PR
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
# Add GitHub remote (if not already done)
|
|
80
|
-
git remote add origin https://github.com/USERNAME/vuer-rtc.git
|
|
81
|
-
|
|
82
|
-
# Push to GitHub
|
|
83
|
-
git push -u origin main
|
|
84
|
-
|
|
85
|
-
# Create PR using GitHub CLI
|
|
86
|
-
gh pr create \
|
|
87
|
-
--title "Phase 1: Foundation - Jest, Vector Clocks, Operations, Prisma" \
|
|
88
|
-
--body "$(cat PHASE1_SUMMARY.md)" \
|
|
89
|
-
--reviewer marvinluo1
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## Next Steps
|
|
93
|
-
|
|
94
|
-
Continue with Phase 2: State Management implementation.
|